一、沉默王二-集合框架
1、java泛型
首先,我们来按照泛型的标准重新设计一下 Arraylist 类。
class Arraylist<E> {
private Object[] elementData;
private int size = 0;
public Arraylist(int initialCapacity) {
this.elementData = new Object[initialCapacity];
}
public boolean add(E e) {
elementData[size++] = e;
return true;
}
E elementData(int index) {
return (E) elementData[index];
}
}
一个泛型类就是具有一个或多个类型变量的类。Arraylist 类引入的类型变量为 E(Element,元素的首字母),使用尖括号 <> 括起来,放在类名的后面。
然后,我们可以用具体的类型(比如字符串)替换类型变量来实例化泛型类。
Arraylist<String> list = new Arraylist<String>();
list.add("沉默王三");
String str = list.get(0);
Date 类型也可以的。
Arraylist<Date> list = new Arraylist<Date>();
list.add(new Date());
Date date = list.get(0);
其次,我们还可以在一个非泛型的类(或者泛型类)中定义泛型方法。
class Arraylist<E> {
public <T> T[] toArray(T[] a) {
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
}
}
不过,说实话,泛型方法的定义看起来略显晦涩。来一副图吧(注意:方法返回类型和方法参数类型至少需要一个)。
现在,我们来调用一下泛型方法。
Arraylist<String> list = new Arraylist<>(4);
list.add("沉");
list.add("默");
list.add("王");
list.add("二");
String [] strs = new String [4];
strs = list.toArray(strs);
for (String str : strs) {
System.out.println(str);
}
1)泛型限定符
在解释这个限定符之前,我们假设有三个类,它们之间的定义是这样的。
class Wanglaoer {
public String toString() {
return "王老二";
}
}
class Wanger extends Wanglaoer{
public String toString() {
return "王二";
}
}
class Wangxiaoer extends Wanger{
public String toString() {
return "王小二";
}
}
我们使用限定符 extends 来重新设计一下 Arraylist 类。
class Arraylist<E extends Wanger> {
}
当我们向 Arraylist 中添加 Wanglaoer 元素的时候,编译器会提示错误:Arraylist 只允许添加 Wanger 及其子类 Wangxiaoer 对象,不允许添加其父类 Wanglaoer。
Arraylist<Wanger> list = new Arraylist<>(3);
list.add(new Wanger());
list.add(new Wanglaoer());
// The method add(Wanger) in the type Arraylist<Wanger> is not applicable for the arguments
// (Wanglaoer)
list.add(new Wangxiaoer());
也就是说,限定符 extends 可以缩小泛型的类型范围。
2)类型擦除
Java 虚拟机会将泛型的类型变量擦除,并替换为限定类型(没有限定的话,就用 Object)。
public class Cmower {
public static void method(Arraylist<String> list) {
System.out.println("Arraylist<String> list");
}
public static void method(Arraylist<Date> list) {
System.out.println("Arraylist<Date> list");
}
}
在浅层的意识上,我们会想当然地认为 Arraylist<String> list 和 Arraylist<Date> list 是两种不同的类型,因为 String 和 Date 是不同的类。
但由于类型擦除的原因,以上代码是不会通过编译的——编译器会提示一个错误(这正是类型擦除引发的那些“问题”)。
也就是说,method(Arraylist<String> list) 和 method(Arraylist<Date> list) 是同一种参数类型的方法,不能同时存在。类型变量 String 和 Date 在擦除后会自动消失,method 方法的实际参数是 Arraylist list。
3)泛型通配符
通配符使用英文的问号(?)来表示。在我们创建一个泛型对象时,可以使用关键字 extends 限定子类,也可以使用关键字 super 限定父类。
// 定义一个泛型类 Arraylist<E>,E 表示元素类型
class Arraylist<E> {
}
利用 <? extends Wanger> 形式的通配符,可以实现泛型的向上转型,来看例子。
Arraylist<? extends Wanger> list2 = new Arraylist<>(4);
list2.add(null);
// list2.add(new Wanger());
// list2.add(new Wangxiaoer());
Wanger w2 = list2.get(0);
// Wangxiaoer w3 = list2.get(1);
list2 的类型是 Arraylist<? extends Wanger>,翻译一下就是,list2 是一个 Arraylist,其类型是 Wanger 及其子类。
注意,list2 并不允许通过 add(E e) 方法向其添加 Wanger 或者 Wangxiaoer 的对象,唯一例外的是 null。
虽然不能通过 add(E e) 方法往 list2 中添加元素,但可以给它赋值。
Arraylist<Wanger> list = new Arraylist<>(4);
Wanger wanger = new Wanger();
list.add(wanger);
Wangxiaoer wangxiaoer = new Wangxiaoer();
list.add(wangxiaoer);
Arraylist<? extends Wanger> list2 = list;
Wanger w2 = list2.get(1);
System.out.println(w2);
System.out.println(list2.indexOf(wanger));
System.out.println(list2.contains(new Wangxiaoer()));
Arraylist<? extends Wanger> list2 = list; 语句把 list 的值赋予了 list2,此时 list2 == list。由于 list2 不允许往其添加其他元素,所以此时它是安全的——我们可以从容地对 list2 进行 get()、indexOf() 和 contains()。
2、迭代器Iterator和Iterable
在 Java 中,我们对 List 进行遍历的时候,主要有这么三种方式。
第一种:for 循环。
for (int i = 0; i < list.size(); i++) {
System.out.print(list.get(i) + ",");
}
第二种:迭代器。
Iterator it = list.iterator();
while (it.hasNext()) {
System.out.print(it.next() + ",");
}
第三种:for-each。
for (String str : list) {
System.out.print(str + ",");
}
第一种我们略过,第二种用的是 Iterator,第三种看起来是 for-each,其实背后也是 Iterator,看一下反编译后的代码(如下所示)就明白了。
Iterator var3 = list.iterator();
while(var3.hasNext()) {
String str = (String)var3.next();
System.out.print(str + ",");
}
for-each 只不过是个语法糖,让我们开发者在遍历 List 的时候可以写更少的代码,更简洁明了。
Iterator 是个接口,JDK 1.2 的时候就有了,用来改进 Enumeration 接口:
- 允许删除元素(增加了 remove 方法)
- 优化了方法名(Enumeration 中是 hasMoreElements 和 nextElement,不简洁)
来看一下 Iterator 的源码:
public interface Iterator<E> {
// 判断集合中是否存在下一个对象
boolean hasNext();
// 返回集合中的下一个对象,并将访问指针移动一位
E next();
// 删除集合中调用next()方法返回的对象
default void remove() {
throw new UnsupportedOperationException("remove");
}
}
Iterable 接口中新增了 forEach 方法。该方法接受一个 Consumer 对象作为参数,用于对集合中的每个元素执行指定的操作。它对 Iterable 的每个元素执行给定操作,具体指定的操作需要自己写Consumer接口通过accept方法回调出来。
List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3));
list.forEach(integer -> System.out.println(integer));
写得更浅显易懂点,就是:
List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3));
list.forEach(new Consumer<Integer>() {
@Override
public void accept(Integer integer) {
System.out.println(integer);
}
});
如果我们仔细观察ArrayList 或者 LinkedList 的“户口本”就会发现,并没有直接找到 Iterator 的影子。
List 的关系图谱中并没有直接使用 Iterator,而是使用 Iterable 做了过渡。
Map 就没办法直接使用 for-each,因为 Map 没有实现 Iterable 接口,只有通过 map.entrySet()、map.keySet()、map.values() 这种返回一个 Collection 的方式才能 使用 for-each。
如果我们仔细研究 LinkedList 的源码就会发现,LinkedList 并没有直接重写 Iterable 接口的 iterator 方法,而是由它的父类 AbstractSequentialList 来完成。
我们知道,集合(Collection)不仅有 List,还有 Set,那 Iterator 不仅支持 List,还支持 Set,但 ListIterator 就只支持 List。
那可能有些小伙伴会问:为什么不直接让 List 实现 Iterator 接口,而是要用内部类来实现呢?
这是因为有些 List 可能会有多种遍历方式,比如说 LinkedList,除了支持正序的遍历方式,还支持逆序的遍历方式。
3.foreach陷阱
1)报错原因
为什么阿里的 Java 开发手册里会强制不要在 foreach 里进行元素的删除操作?
是因为 for-each 本质上是个语法糖,底层是通过迭代器 Iterator 配合 while 循环实现的,来看一下反编译后的字节码。
List<String> list = new ArrayList();
list.add("沉默王二");
list.add("沉默王三");
list.add("一个文章真特么有趣的程序员");
Iterator var2 = list.iterator();
while(var2.hasNext()) {
String str = (String)var2.next();
if ("沉默王二".equals(str)) {
list.remove(str);
}
}
System.out.println(list);
为什么不能在foreach里执行删除操作?
因为 foreach 循环是基于迭代器实现的,而迭代器在遍历集合时会维护一个 expectedModCount 属性来记录集合被修改的次数。如果在 foreach 循环中执行删除操作会导致 expectedModCount 属性值与实际的 modCount 属性值不一致,从而导致迭代器的 hasNext() 和 next() 方法抛出 ConcurrentModificationException 异常。
这种机制可以保证迭代器在遍历 ArrayList 时,不会遗漏或重复元素,同时也可以在多线程环境下检测到并发修改问题。
那其实在阿里巴巴的 Java 开发手册里也提到了,不要在 for-each 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator 方式。
2)如何正确地删除
1、remove 后 break
List<String> list = new ArrayList<>();
list.add("沉默王二");
list.add("沉默王三");
list.add("一个文章真特么有趣的程序员");
for (String str : list) {
if ("沉默王二".equals(str)) {
list.remove(str);
break;
}
}
break 后循环就不再遍历了,意味着 Iterator 的 next 方法不再执行了,也就意味着 checkForComodification 方法不再执行了,所以异常也就不会抛出了。
但是呢,当 List 中有重复元素要删除的时候,break 就不合适了。
2、for 循环
List<String> list = new ArrayList<>();
list.add("沉默王二");
list.add("沉默王三");
list.add("一个文章真特么有趣的程序员");
for (int i = 0; i < list.size(); i++) {
String str = list.get(i);
if ("沉默王二".equals(str)) {
list.remove(str);
}
}
for 循环虽然可以避开 fail-fast 保护机制,也就说 remove 元素后不再抛出异常;但是呢,这段程序在原则上是有问题的。为什么呢?
第一次循环的时候,i 为 0,list.size() 为 3,当执行完 remove 方法后,i 为 1,list.size() 却变成了 2,因为 list 的大小在 remove 后发生了变化,也就意味着“沉默王三”这个元素被跳过了。能明白吗?
remove 之前 list.get(1) 为“沉默王三”;但 remove 之后 list.get(1) 变成了“一个文章真特么有趣的程序员”,而 list.get(0) 变成了“沉默王三”。
3、使用 Iterator
List<String> list = new ArrayList<>();
list.add("沉默王二");
list.add("沉默王三");
list.add("一个文章真特么有趣的程序员");
Iterator<String> itr = list.iterator();
while (itr.hasNext()) {
String str = itr.next();
if ("沉默王二".equals(str)) {
itr.remove();
}
}
为什么使用 Iterator 的 remove 方法就可以避开 fail-fast 保护机制呢?看一下 remove 的源码就明白了。
public void remove() {
if (lastRet < 0) // 如果没有上一个返回元素的索引,则抛出异常
throw new IllegalStateException();
checkForComodification(); // 检查 ArrayList 是否被修改过
try {
ArrayList.this.remove(lastRet); // 删除上一个返回元素
cursor = lastRet; // 更新下一个元素的索引
lastRet = -1; // 清空上一个返回元素的索引
expectedModCount = modCount; // 更新 ArrayList 的修改次数
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException(); // 抛出异常
}
}
删除完会执行 expectedModCount = modCount,保证了 expectedModCount 与 modCount 的同步。
二、小博哥-开发基础
1、Spring Dependency Injection依赖注入使用技巧
实例化注解:
@Component:组件注解@Service:服务注解@Repository:仓储注解,提供对持久化类数据的操作的服务。@Controller/@RestController():对外提供服务的注解。
在 Spring 框架中,依赖注入(DI)是通过一系列的步骤和组件来实现的。对于构造函数注入,特别是注入 Map 类型的依赖,Spring 需要处理以下几个关键步骤:
- Bean Definition 解析:Spring 解析配置文件或注解,生成 BeanDefinition 对象。
- Bean 实例化:Spring 根据 BeanDefinition 创建 Bean 实例。
- 依赖注入:Spring 将所需的依赖注入到 Bean 中。
具体流程:
- 解析 BeanDefinition: Spring 解析配置文件或注解,生成
AwardController的BeanDefinition对象。 - 选择构造函数:
AutowiredAnnotationBeanPostProcessor会扫描AwardController的构造函数,发现它有一个Map<String, IAwardService>类型的参数。 - 查找依赖:
ConstructorResolver会根据构造函数参数的类型,查找 Spring 容器中所有IAwardService类型的 Bean,并将它们放入一个Map中。这个Map的键是 Bean 的名称,值是对应的IAwardService实例。 - 实例化 Bean:
ConstructorResolver使用找到的依赖,调用AwardController的构造函数,创建AwardController实例。 - 注入依赖:
DefaultListableBeanFactory将创建好的Map<String, IAwardService>注入到AwardController的构造函数中。
1)空注入判断
- 场景:NullAwardService 没有配置 @Service 注册,或者在程序中手动实例化的这个 Bean 对象,根据不同诉求,在没有创建的时候。可以使用
@Autowired(required = false)进行注入。这样就不会报错 nullAwardService 空指针异常。 - 用途:当我们在使用支付、openai外部接口对接测试阶段,可能有些时候是需要关闭服务的,也就是不实例化对象。那么这个时候就配置
@Autowired(required = false)避免注入空指针。
2)优先实例化
@Slf4j
@Service("openai_model")
// Primary 首选 Bean 对象标记
@Primary
@Order(1)
- 场景:一个 IAwardService 有多个实现类的时候,如果还想用
@Resource 注入 awardService的时候是会报错说NoUniqueBeanDefinitionException异常了。这个时候使用 @Primary 就会标记为首选对象,注入的时候会注入这个对象。另外这里的@Order(1)是对象的加载顺序。 - 用途:当我们为一组接口提供实现类,并需要提供默认的注入的时候,就可以使用
@Primary注解来限定首选注入项。
3)检测创建,避免重复
- 场景:
@Bean可以用于在方法,创建出对象。这有点类似于使用 Spring 的 FactoryBean 接口创建对象一样,这里可以直接使用方法创建。之后@ConditionalOnMissingBean注解的目的是为了避免重复创建,判断应用上下文中存在这个对象,则不会重复创建。 - 用途:通常我们在做一些组件的时候,会加入这样一个注解,避免在业务工程中引入同类的组件的时候,会导致创建出相同对象而报错。
4)配置是否创建对象
@Bean
@ConditionalOnProperty(value = "sdk.config.enabled", havingValue = "true", matchIfMissing = false)
- 场景:模拟创建 createTopic,入参的对象为注入的操作,
@Qualifier注解可以指定要注入哪个名字的对象。之后@ConditionalOnProperty注解可以通过配置的 enabled 值,来确定是否实例化对象。 - 用途:这个场景是非常实用的,比如你做了一个组件,或者业务中要增加一些配置。启动或关闭某些服务,就可以使用了。而不需要把 pom 中引入的组件注释掉。
5)自定义Condition,判断是否实例化对象
- 场景:是一个案例中使用到了
@ConditionalOnProperty注解,我们也可以自定义一个 Conditional 的实现类,之后把这个实现类配置到需要实例化的对象上面,通过 matches 匹配条件方法的实现,决定是否实例化。 - 用途:这个场景的用途和
@ConditionalOnProperty是一样的,只不过我们可以更好的自定义控制。
6)根据环境配置实例化对象
@Slf4j
@Service
// 用于根据配置环境实例化 Bean 对象
@Profile({"prod", "test"})
@Lazy
- 场景:
@Profile({"prod", "test"})注解可以配置你是在什么时候实例化这个对象,我们可以指定 application.yml 中配置的active: dev/prod/test来确定是在开发、测试还是上线才实例化这个对象。 - 用途:一些只有到线上才能实例化对象的时候,就可以配置
@Profile({"prod", "test"})注解,注意这个需要配合@Autowired(required = false)进行注入,否则会出现注入为空指针的异常。
7)引入 Spring 配置
- 场景:在 SpringBoot 工程中,可以通过
@ImportResource、@PropertySource引入对应的配置文件,完成对象的初始化。 - 用途:在实际的开发中,虽然使用 SpringBoot 工程,但为了兼容一些老的项目或者一些还没有升级到 SpringBoot Starter 的组件,则需要单独引入 Spring 配置文件来创建对象。
8)原型对象
- 场景:
@Scope("prototype")可以设定对象类型为原型对象,每次获得的对象都是一个新的实例化对象。 - 用途:对于动态,不同责任链创建,可以使用这个配置,确保每个对象都是自己的。