博客记录-day011-java泛型、迭代器Iterator和Iterable、foreach陷阱

167 阅读13分钟

一、沉默王二-集合框架

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 需要处理以下几个关键步骤:

  1. Bean Definition 解析:Spring 解析配置文件或注解,生成 BeanDefinition 对象。
  2. Bean 实例化:Spring 根据 BeanDefinition 创建 Bean 实例。
  3. 依赖注入:Spring 将所需的依赖注入到 Bean 中。

具体流程:

  1. 解析 BeanDefinition: Spring 解析配置文件或注解,生成 AwardController 的 BeanDefinition 对象。
  2. 选择构造函数: AutowiredAnnotationBeanPostProcessor 会扫描 AwardController 的构造函数,发现它有一个 Map<String, IAwardService> 类型的参数。
  3. 查找依赖: ConstructorResolver 会根据构造函数参数的类型,查找 Spring 容器中所有 IAwardService 类型的 Bean,并将它们放入一个 Map 中。这个 Map 的键是 Bean 的名称,值是对应的 IAwardService 实例。
  4. 实例化 Bean: ConstructorResolver 使用找到的依赖,调用 AwardController 的构造函数,创建 AwardController 实例。
  5. 注入依赖: 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") 可以设定对象类型为原型对象,每次获得的对象都是一个新的实例化对象。
  • 用途:对于动态,不同责任链创建,可以使用这个配置,确保每个对象都是自己的。