JDK集合类源码 || 三、Iterator、fail-fast机制、比较器

 ̄綄美尐妖づ 提交于 2019-11-27 05:24:10


前言

这一系列是对程序员江湖博主LXF - Java集合类专题的整理。因为原文虽然讲解的很全面透彻,但是组织结构不清晰,而且错别字较多,所以我重新整理,便于自己阅读理解记忆。

原文:Java集合详解3:Iterator,fail-fast机制与比较器
原文参考:Java技术驿站的 Java提高篇(三十)—–IteratorJava提高篇(三四)—–fail-fast机制

一、Iterator简单说明

Java程序员对于迭代器绝对不陌生,因为我们需要经常使用JDK提供的迭代器接口进行Java集合的迭代。

Iterator iterator = list.iterator();

while(iterator.hasNext()){
    String string = iterator.next();
    //do something
}

其实我们可以简单地将迭代理解为遍历,是一个标准化遍历各类容器里面的所有对象的方法类。它是一个很典型的设计模式。

迭代器可以把访问逻辑从不同类型的集合类中抽象出来,从而避免向客户端暴露集合的内部结构。 如果不使用迭代器,我们都是如下进行处理,

  • 对于数组,使用下标来进行遍历
int[] arrays = new int[10];
for(int i = 0 ; i < arrays.length ; i++){
    int a = arrays[i];
    //do something
}
  • 对于ArrayList,使用get()方法进行遍历
List<String> list = new ArrayList<String>();
for(int i = 0 ; i < list.size() ;  i++){
    String string = list.get(i);
    //do something
 }

对于这两种方式,我们总是事先知道集合的内部结构,访问代码和集合本身是紧密耦合的,无法将访问逻辑从集合类和客户端代码中分离出来。同时每一种集合对应一种遍历方法,客户端代码无法复用。

在实际应用中,如果需要整合上面两个集合是相当麻烦的。所以为了解决这个问题,Iterator模式 腾空出世,它总是用同一种逻辑来遍历集合。使得客户端自身不再需要维护集合的内部结构,所有的内部结构都由Iterator来维护。客户端不直接和集合类打交道,它总是控制Iterator,向它发送"向前",“向后”,"取当前元素"的命令,就可以间接遍历整个集合。

上面只是对 Iterator 模式简单的说明,下面我们看看 Java 中 Iterator 接口,看其是如何实现的。

二、java.util.Iterator

在 Java 中 Iterator 是一个接口,它只提供了迭代了基本规则,在JDK中它是这样定义的:对 collection 进行迭代的迭代器。Iterator 取代了 Java 集合框架中的 Enumeration。迭代器与枚举有两点不同,

1、迭代器允许调用者利用定义良好的语义,在迭代期间从迭代器所指向的 collection 中移除元素
2、方法名称得到了改进

Iterator 接口的定义如下所示:

public interface Iterator {
  boolean hasNext();
  Object next();
  void remove();
}

其中,

boolean hasNext():判断容器内是否还有可供访问的元素

Object next():返回迭代器刚越过的元素的引用。返回值是Object,需要强制转换成自己需要的类型

void remove():删除迭代器刚越过的元素

通常,我们只需要使用next()、hasNext()两个方法即可完成迭代。如下:

for(Iterator it = c.iterator(); it.hasNext(); ) {
  Object o = it.next();
  //do something
}

Iterator it = c.itrator();
while(it.hasNext()) {
	Object o = it.next();
	//do something
}

前面介绍过,Iterator 有一个很大的优点就是,我们不必知道集合的内部结构、状态由 Iterator 维持,通过统一的方法hasNext()、next()来判断、获取下一个元素。至于内部的具体实现我们不必关心。

但是作为一个合格的程序员,非常有必要弄清楚 Iterator 的实现。下面使用 ArrayList 的源码进行分析。

三、各个集合的Iterator的实现

下面以 ArrayList 的 Iterator 实现来分析。其实如果我们理解了ArrayList、Hashset、TreeSet的数据结构及内部实现,对于它们是如何实现Iterator 也会胸有成竹。因为 ArrayList 的底层采用数组实现,所以只需要记录相应位置的索引即可,其 iterator() 方法的实现就比较简单。

1、 ArrayList的Iterator实现

在 ArrayList 内部,首先定义了一个内部类 Itr,该内部类实现了 Iterator 接口,如下:

private class Itr implements Iterator<E> {
    //do something
}

ArrayList 的 iterator()方法实现:
public Iterator<E> iterator() {
	return new Itr();
}

由上可知,使用 ArrayList 的 iterator() 方法返回的是 Itr 内部类的实例对象,所以真正需要关心的是 Itr() 内部类的实现

在 Itr 内部定义了三个 int 型的变量:cursor、lastRet、expectedModCount。其中 cursor 表示下一个元素的索引位置,lastRet 表示上一个元素的索引位置。如下:

int cursor;             
int lastRet = -1;     
int expectedModCount = modCount;

从 cursor、lastRet 的定义可以看出,lastRet 一直比 cursor 小。所以 hasNext() 方法的实现非常简单,只需要判断 cursor 和 size 是否相等即可。如下:

public boolean hasNext() {
    return cursor != size;
}

next() 方法的实现其实也比较简单,只要返回 cursor 索引位置处的元素,然后修改cursor、lastRet即可。如下:

public E next() {
    checkForComodification();
    int i = cursor;    // 记录索引位置
    if (i >= size)     // 如果获取元素大于集合元素个数,则抛出异常
        throw new NoSuchElementException();
    Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length)
        throw new ConcurrentModificationException();
    cursor = i + 1;      // cursor移到下一个位置
    return (E) elementData[lastRet = i];  // lastRet + 1 且返回i(之前cursor)处元素
}

checkForComodification() 方法主要用来判断集合的修改次数是否合法,即用来判断遍历过程中集合是否被修改过。代码如下所示。

modCount 用于记录 ArrayList 集合的修改次数,初始化为0。每当集合被修改一次(结构上面的修改,内部update不算),如add、remove等方法,则 modCount + 1。所以如果 modCount 不变,则表示集合内容没有被修改。

该机制主要是用于实现 ArrayList 集合的快速失败机制。在Java的集合中,较大一部分集合是存在快速失败机制的,这里不多说,后面会讲到。所以如果要保证在遍历过程中不出现错误,就应该保证在遍历过程中不会对集合产生结构上的修改(当然remove方法除外)。出现了异常错误,我们就应该认真检查程序是否出错而不是catch后不做处理。

final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

remove()方法的实现,是调用ArrayList本身的remove()方法删除lastRet位置元素,然后修改modCount即可。
public void remove() {
    if (lastRet < 0)
        throw new IllegalStateException();
    checkForComodification();

    try {
        ArrayList.this.remove(lastRet);
        cursor = lastRet;
        lastRet = -1;
        expectedModCount = modCount;
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}

对 ArrayList 的 Iterator 的实现就讲解到这里,对于Hashset、TreeSet等集合的 Iterator 实现,如果感兴趣可以继续研究,个人认为在研究这些集合的源码之前,有必要对该集合的数据结构有清晰的认识,这样会达到事半功倍的效果!

四、fail-fast机制

原文参考了以下文章:http://cmsblogs.com/?p=1220

在JDK的 Collection 中,我们经常会看到类似于下面的话,

例如,在 ArrayList 中:

注意,迭代器的快速失败行为不能得到保证,因为一般来说,存在非同步的并发修改时,不可能作出任何硬性的保证。快速失败迭代器会尽最大努力抛出 ConcurrentModificationException。因此,为提高这类迭代器的正确性而编写依赖于此异常的程序的做法是错误的,正确做法是:迭代器的快速失败行为应该仅用于检测程序错误。

在HashMap中:

注意,迭代器的快速失败行为不能得到保证,因为一般来说,存在非同步的并发修改时,不可能作出任何硬性的保证。快速失败迭代器会尽最大努力抛出 ConcurrentModificationException。因此,为提高这类迭代器的正确性而编写依赖于此异常的程序的做法是错误的,正确做法是:迭代器的快速失败行为应该仅用于检测程序错误。

在这两段话中反复提到了”快速失败”。那么,什么是快速失败机制呢?

快速失败也就是 fail-fast,它是Java集合的一种错误检测机制。当多个线程对集合进行结构上的改变时,有可能会产生fail-fast机制。记住是有可能,而不是一定。

例如:假设存在两个线程(线程1、线程2),线程1通过 Iterator 遍历集合 A 中的元素,在某个时刻线程2修改了集合 A 的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这时程序就会抛出ConcurrentModificationException异常,从而产生fail-fast机制。

1、fail-fast示例

public class FailFastTest {
    private static List<Integer> list = new ArrayList<>();
    
    /
     * @desc:线程one迭代list
     * @Project:test
     * @file:FailFastTest.java
     * @Authro:chenssy
     * @data:2014726*/
    private static class threadOne extends Thread{
        public void run() {
            Iterator<Integer> iterator = list.iterator();
            while(iterator.hasNext()){
                int i = iterator.next();
                System.out.println("ThreadOne 遍历:" + i);
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    /
     * @desc:当i == 3时,修改list
     * @Project:test
     * @file:FailFastTest.java
     * @Authro:chenssy
     * @data:2014726*/
    private static class threadTwo extends Thread{
        public void run(){
            int i = 0 ; 
            while(i < 6){
                System.out.println("ThreadTwo run:" + i);
                if(i == 3){
                    list.remove(i);
                }
                i++;
            }
        }
    }
    
    public static void main(String[] args) {
        for(int i = 0 ; i < 10;i++){
            list.add(i);
        }
        new threadOne().start();
        new threadTwo().start();
    }
}

运行结果:

ThreadOne 遍历:0
ThreadTwo run:0
ThreadTwo run:1
ThreadTwo run:2
ThreadTwo run:3
ThreadTwo run:4
ThreadTwo run:5
Exception in thread "Thread-0" java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(Unknown Source)
    at java.util.ArrayList$Itr.next(Unknown Source)
    at test.ArrayListTest$threadOne.run(ArrayListTest.java:23)

2、fail-fast产生原因

通过上面的示例和讲解,我们初步了解到 fail-fast 产生的原因在于,程序在对 collection 进行迭代时,某个线程在该 collection 的结构上做了修改,这时迭代器就会抛出 ConcurrentModificationException 异常,从而产生 fail-fast。

要了解 fail-fast 机制,我们首先要对 ConcurrentModificationException 异常有所了解。当方法检测到对象的并发修改,但不允许这种修改时就抛出该异常。
同时需要注意的是,该异常不会始终指出对象已经由不同线程并发修改,如果单线程违反了规则,同样也有可能会抛出改异常。

诚然,迭代器的快速失败行为无法得到保证,它不能保证一定会出现该错误,但是快速失败操作会尽最大努力抛出ConcurrentModificationException异常,因此,为提高此类操作的正确性而编写一个依赖于此异常的程序是错误的做法,正确做法是:ConcurrentModificationException 应该仅用于检测 bug。下面以 ArrayList 为例进一步分析 fail-fast 产生的原因。

由上可知,fail-fast 是在操作迭代器时产生的。现在来看 ArrayList 中迭代器的源代码

private class Itr implements Iterator<E> {
    int cursor;
    int lastRet = -1;
    int expectedModCount = ArrayList.this.modCount;

    public boolean hasNext() {
        return (this.cursor != ArrayList.this.size);
    }

    public E next() {
        checkForComodification();
        /** 省略此处代码 */
    }

    public void remove() {
        if (this.lastRet < 0)
            throw new IllegalStateException();
        checkForComodification();
        /** 省略此处代码 */
    }

    final void checkForComodification() {
        if (ArrayList.this.modCount == this.expectedModCount)
            return;
        throw new ConcurrentModificationException();
    }
}

从上面的源代码可以看出,迭代器在调用 next()、remove() 方法时都调用了 checkForComodification() 方法,该方法主要就是检测modCount == expectedModCount ? 若不等则抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。所以要弄清楚为什么会产生fail-fast机制就必须要弄明白,为什么 modCount != expectedModCount ,它们的值是在什么时候发生改变的。

expectedModCount 是在 Itr 中定义的:

int expectedModCount = ArrayList.this.modCount;

所以它的值是不可能被修改的,可能会被修改的是 modCount。modCount 是在 AbstractList 中定义的,为全局变量:

protected transient int modCount = 0;

那么,modCount 在何时会因为什么原因而发生改变呢?看 ArrayList 的源码:

public boolean add(E paramE) {
    ensureCapacityInternal(this.size + 1);
    /** 省略此处代码 */
}

private void ensureCapacityInternal(int paramInt) {
    if (this.elementData == EMPTY_ELEMENTDATA)
        paramInt = Math.max(10, paramInt);
    ensureExplicitCapacity(paramInt);
}

private void ensureExplicitCapacity(int paramInt) {
    this.modCount += 1;    //修改modCount
    /** 省略此处代码 */
}

public boolean remove(Object o) {
    if (o == null) {
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                fastRemove(index);
                return true;
            }
    } else {
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
    }
    return false;
}

private void fastRemove(int paramInt) {
    this.modCount += 1;   /修改modCount
    /** 省略此处代码 */
}

public void clear() {
    this.modCount += 1;    /修改modCount
    /** 省略此处代码 */
}

总结:从上面的源码可以看出,ArrayList 中无论add、remove、clear方法,只要是改变 ArrayList 元素个数的方法都会导致modCount的改变。

所以,可以初步判断由于 expectedModCount 的值与 modCount 改变的值不同步,导致两者不等从而产生 fail-fast 机制。知道了产生 fail-fast 的根本原因了,可以有如下场景:

有两个线程(线程A,线程B),其中线程A遍历list、线程B修改list。线程A在遍历list的某个时候(此时expectedModCount = modCount=N),线程B启动,同时线程B增加一个元素,这时 modCount 的值发生改变(modCount + 1 = N + 1)。线程A继续遍历执行 next 方法时,通过 checkForComodification 方法发现 expectedModCount = N ,而 modCount = N + 1,两者不等,这时就抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。

至此,我们已经完全了解了 fail-fast 产生的根本原因了。知道了原因容易解决了。

3、fail-fast解决办法

通过前面的实例及源码分析,已经基本了解了 fail-fast 机制,下面就产生的原因提出两种解决方案:

方案一:在遍历过程中,所有涉及改变 modCount 值的地方,全部加上 synchronized关键字 或者直接使用 Collections.synchronizedList,这样就可以解决。但是不推荐,因为增删造成的同步锁可能会阻塞遍历操作。

方案二:使用 CopyOnWriteArrayList 来替换 ArrayList。推荐使用该方案。

CopyOnWriteArrayList 是 ArrayList 的一个线程安全的变体,其中所有可变操作(add、set 等等)都是通过对底层数组进行一次新的复制来实现的。 该类产生的开销比较大,但是在以下两种情况,它非常适合使用。

1、CopyOnWriterArrayList 的数据结构、定义都和 ArrayList 一样。它同样实现了 List 接口,底层使用数组实现。在方法上也包含add、remove、clear、iterator等方法。

2、CopyOnWriterArrayList 根本就不会产生 ConcurrentModificationException 异常,也就是说,它使用迭代器完全不会产生 fail-fast 机制。

源码如下:

private static class COWIterator implements ListIterator {
	/** 省略此处代码 */
	public E next() {
		if (!(hasNext()))
			throw new NoSuchElementException();
		return this.snapshot[(this.cursor++)];
	}
	/** 省略此处代码 */
}

CopyOnWriterArrayList 的方法,没有像 ArrayList 使用 checkForComodification 方法来判断 expectedModCount 与 modCount 是否相等。为什么这么做呢?以add方法为例:

public boolean add(E paramE) {
    ReentrantLock localReentrantLock = this.lock;
    localReentrantLock.lock();
    try {
        Object[] arrayOfObject1 = getArray();
        int i = arrayOfObject1.length;
        Object[] arrayOfObject2 = Arrays.copyOf(arrayOfObject1, i + 1);
        arrayOfObject2[i] = paramE;
        setArray(arrayOfObject2);
        int j = 1;
        return j;
    } finally {
        localReentrantLock.unlock();
    }
}


final void setArray(Object[] paramArrayOfObject) {
    this.array = paramArrayOfObject;
}

CopyOnWriterArrayList 的 add 方法与 ArrayList 的 add 方法有一个最大的不同点在于下面三句代码:

Object[] arrayOfObject2 = Arrays.copyOf(arrayOfObject1, i + 1);
arrayOfObject2[i] = paramE;
setArray(arrayOfObject2);

正是这三句代码使得 CopyOnWriterArrayList 不会抛 ConcurrentModificationException 异常。它们所展现的魅力就在于复制原来的数组,然后在复制数组上进行 add 操作,这样做就完全不会影响 COWIterator 中的 array 了。

所以 CopyOnWriterArrayList 所代表的核心概念就是:

任何在数组结构上有改变的操作(add、remove、clear等),CopyOnWriterArrayList 都会复制现有的数组,然后在复制的数据上修改,这样就不会影响 COWIterator 中的数据了,修改完成之后改变原有数据的引用即可。同时这样造成的代价就是产生大量的对象,同时数组的复制操作也很耗时。

五、Comparable接口 和 Comparator接口

Java 为我们提供了两种比较机制:ComparableComparator,它们之间有什么区别呢?

1、Comparable接口

Comparable 接口在 java.lang 包下,内部只有一个 compareTo() 方法

Comparable 可以让其实现类的对象进行比较,具体的比较规则定义在 compareTo 方法中。这种顺序称为自然顺序。

compareTo 方法的返回值有三种情况:

e1.compareTo(e2) > 0 即 e1 > e2
e1.compareTo(e2) = 0 即 e1 = e2
e1.compareTo(e2) < 0 即 e1 < e2

注意:

1、由于 null 不是一个类,也不是一个对象,因此在重写 compareTo 方法时应该注意 e.compareTo(null) 的情况,即使 e.equals(null) 返回 false,compareTo 方法也应该主动抛出一个空指针异常 NullPointerException。

2、Comparable 实现类重写 compareTo() 方法时,一般要求 e1.compareTo(e2) == 0 的结果要和 e1.equals(e2) 一致。这样将来使用 SortedSet 等根据类的自然排序进行排序的集合容器时,可以保证保存的数据顺序和想象中一致。

有人可能好奇上面的第二点如果违反了会怎样呢?

举个例子,如果你往一个 SortedSet 中先后添加两个对象 a 和 b,a b 满足 !a.equals(b) && a.compareTo(b) == 0,同时也没有另外指定个 Comparator,那当你添加完 a 再添加 b 时会添加失败返回 false, SortedSet 的 size 也不会增加。因为在 SortedSet 看来它们是相同的,而 SortedSet 中是不允许重复的。

三点总结:

实际上所有实现了 Comparable 接口的 Java 核心类的结果都和 equlas 方法保持一致。

实现了 Comparable 接口的 List 或者数组可以使用 Collections.sort() 或者 Arrays.sort() 方法进行排序。

实现了 Comparable 接口的对象才能够直接被用作 SortedMap (SortedSet) 的 key,否则需要另外指定 Comparator 排序规则。

因此自定义的类如果要使用有序的集合类,需要实现 Comparable 接口,比如:

描述: 测试用的实体类书, 实现了 Comparable 接口,自然排序

public class BookBean implements Serializable, Comparable {
	private String name;
	private int count;

	public BookBean(String name, int count) {
		this.name = name;
		this.count = count;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public int getCount() {
		return count;
	}

	public void setCount(int count) {
		this.count = count;
	}

	/
	重写 equals 方法
	*/
	@Override
	public boolean equals(Object o) {
		if (this == o) return true;
		if (!(o instanceof BookBean)) return false;

		BookBean bean = (BookBean) o;

		if (getCount() != bean.getCount()) return false;
		return getName().equals(bean.getName());
	}
	
	/
	重写 hashCode 的计算方法
	根据所有属性进行迭代计算,避免重复
	计算 hashCode 时 计算因子 31 见得很多,是一个质数,不能再被除
	*/
	@Override
	public int hashCode() {
		//调用 String 的 hashCode(), 唯一表示一个字符串内容
		int result = getName().hashCode();
		//乘以 31, 再加上 count
		result = 31 * result + getCount();
		return result;
	}

	@Override
	public String toString() {
		return “BookBean{+ “name=’” + name + ‘’’ +, count=+ count +};
	}
	
	/
	当向 TreeSet 中添加 BookBean 时,会调用这个方法进行排序
	*/
	@Override
	public int compareTo(Object another) {
		if (another instanceof BookBean){
			BookBean anotherBook = (BookBean) another;
			int result;

		 	//比如这里按照书价排序
		 	result = getCount() - anotherBook.getCount();   
		 	//或者按照 String 的比较顺序
			//result = getName().compareTo(anotherBook.getName());
		 	if (result == 0){   //当书价一致时,再对比书名。 保证所有属性比较一遍
	    		result = getName().compareTo(anotherBook.getName());
		 	}
		 	return result;
		 }
		 // 一样就返回 0
		return 0;
	}
}

上述代码中还重写了 equlas()、hashCode() 方法,自定义类将来可能会进行比较,建议重写这些方法。

这里想表达的是,在有些场景下 equals() 和 compareTo() 结果要保持一致,这时如果不重写 equals(),使用 Object.equals() 方法得到的结果会有问题,比如调用 HashMap.put() 方法,会先调用 key 的 equals() 方法进行比较,然后才调用 compareTo() 方法。
后面重写 compareTo 时,要判断某个相同时对比下一个属性,把所有属性都比较一次。

3、Comparator接口

Comparable 接口属于 Java 集合框架的一部分,用来定制排序规则。Comparator 接口在 java.util 包下,JDK1.8以前只有两个方法:

public interface Comparator<T> {
    public int compare(T lhs, T rhs);
    public boolean equals(Object object);
}

JDK1.8以后又新增了很多方法,基本上都是跟 Function 相关的,这里暂不介绍JDK1.8新增的方法。

从上面内容可知,使用自然排序需要自定义类实现 Comparable 接口,并且需要在内部重写 comparaTo() 方法。

而 Comparator 接口则是在外部制定排序规则,然后作为排序策略参数传递给某些类,比如 Collections.sort()、Arrays.sort(),或者一些内部有序的集合(比如 SortedSet,SortedMap 等)。

Comparator 接口的使用方式主要分三步:

1、创建一个 Comparator 接口的实现类,在 compare() 方法中针对自定义类写排序规则

2、将 Comparator 接口的实现类对象作为策略参数传递给排序类的某个方法

3、向排序类中添加 compare() 方法中使用的自定义类对象

举个例子:

1. 使用匿名内部类的方式创建一个实现 Comparator 接口的子类对象
Comparator comparator = new Comparator() {
    @Override
    public int compare(Object object1, Object object2) {
        if (object1 instanceof NewBookBean && object2 instanceof NewBookBean){
            NewBookBean newBookBean = (NewBookBean) object1;
            NewBookBean newBookBean1 = (NewBookBean) object2;
            //具体比较方法参照 自然排序的 compareTo 方法,这里只举个栗子
            return newBookBean.getCount() - newBookBean1.getCount();
        }
        return 0;
    }
};

2. 将此对象作为形参传递给 TreeSet 的构造器
TreeSet treeSet = new TreeSet(comparator);

3. 向 TreeSet 中添加 步骤 1 中 compare 方法中设计的类的对象
treeSet.add(new NewBookBean("A",34));
treeSet.add(new NewBookBean("S",1));
treeSet.add( new NewBookBean("V",46));
treeSet.add( new NewBookBean("Q",26));

其实可以看出,Comparator 接口的使用是一种策略模式。排序类中持有一个 Comparator 接口的引用:

Comparator<? super K> comparator;

那么,我们就可以传入各种自定义排序规则的 Comparator 实现类的对象,对同样的类制定不同的排序策略。

3、总结

Java中的两种排序方式:

1、Comparable 接口实现自然排序。(实体类实现)
2、Comparator 接口实现定制排序。(无法修改实体类时,直接在调用方通过匿名内部类的方式创建)

同时存在时采用 Comparator(定制排序)的规则进行比较。

对于一些普通的数据类型(比如 String, Integer, Double…),它们默认实现了Comparable 接口,重写了 compareTo 方法,我们可以直接使用;而对于一些自定义类,它们可能在不同情况下需要实现不同的比较策略,我们可以实现 Comparator 接口,然后使用特定的 Comparator 实现进行比较。

这就是 Comparable 和 Comparator 的区别。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!