集合框架与泛型

集合框架和泛型

本章技能目标

  • 掌握集合框架包含的内容
  • 掌握ArrayList和LinkedList的使用
  • 掌握HashMap的使用
  • 掌握Iterator的使用
  • 掌握泛型集合的使用

1. 集合框架概述

Java集合框架为我们提供了一套性能优良、使用方便的接口和类,它们都位于java.util包中。Java集合框架包含的主要内容以及彼此之间的关系如图所示:

在这里插入图片描述

从上面的集合框架图可以看到,Java 集合框架主要包括两种类型的容器,一种是集合(Collection),存储一个元素集合,另一种是图(Map),存储键/值对映射。Collection 接口又有 3 种子类型,List、Set 和 Queue,再下面是一些抽象类,最后是具体实现类,常用的有 ArrayListLinkedListHashSet、LinkedHashSet、HashMap、LinkedHashMap 等等。

集合框架是一个用来代表和操纵集合的统一架构。所有的集合框架都包含如下内容:

  • **接口:**是代表集合的抽象数据类型。例如 Collection、List、Set、Map 等。之所以定义多个接口,是为了以不同的方式操作集合对象
  • **实现(类):**是集合接口的具体实现。从本质上讲,它们是可重复使用的数据结构,例如:ArrayList、LinkedList、HashSet、HashMap。
  • **算法:**是实现集合接口的对象里的方法执行的一些有用的计算,例如:搜索和排序。这些算法被称为多态,那是因为相同的方法可以在相似的接口上有着不同的实现。

除了集合,该框架也定义了几个 Map 接口和类。Map 里存储的是键/值对。尽管 Map 不是集合,但是它们完全整合在集合中。

集合框架体系如图所示:

在这里插入图片描述

Java 集合框架提供了一套性能优良,使用方便的接口和类,java集合框架位于java.util包中, 所以当使用集合框架的时候需要进行导包。

集合框架定义了一些接口。下图提供了每个接口的概述:

序号接口描述
1Collection 接口 Collection 是最基本的集合接口,一个 Collection 代表一组 Object,即 Collection 的元素, Java不提供直接继承自Collection的类,只提供继承于的子接口(如List和set)。Collection 接口存储一组不唯一,无序的对象。
2List 接口 List接口是一个有序的 Collection,使用此接口能够精确的控制每个元素插入的位置,能够通过索引(元素在List中位置,类似于数组的下标)来访问List中的元素,第一个元素的索引为 0,而且允许有相同的元素。List 接口存储一组不唯一,有序(插入顺序)的对象。
3Set Set 具有与 Collection 完全一样的接口,只是行为上不同,Set 不保存重复的元素。Set 接口存储一组唯一,无序的对象。
4Map Map 接口存储一组键值对象,提供key(键)到value(值)的映射。
8Iterator 接口 是负责定义访问和遍历元素的接口。

在集合框架中,List可以理解为前面讲过的数组,元素的内容可以重复并且有序,如下图所示:

在这里插入图片描述

Set可以理解为数学中的集合,里面数据不重复且无序,如下图所示:

在这里插入图片描述

Map也可以理解为数学中的集合,只是其中每个元素都由key和value两个对象组成,提供key到value的映射,如图所示:

在这里插入图片描述

2. List接口

2.1 List接口简介

  1. List接口继承了Collention接口(继承Iterable)

  2. 可以允许重复的对象,可以插入多个null元素,输出的顺序就是插入的顺序

  3. 只能保存对象类型的数据,基本类型的数据会自动装箱成包装类

  4. 实现类有ArrayListLinkedListVactorStackArrayList 最为流行,它对数组进行了封装,实现了长度可变的数组,存储数据的方式与数组相同,都是在内存中分配连续的空间。可以使用索引的随意访问。它的优点在于遍历元素和访问元素的效率比较高。如图所示:在这里插入图片描述

    LinkedList 采用链表存储方式。插入、删除元素时效率比较高。它提供了额外的addFirst()、addLast()、removeFirst()和removeLast()等方法,可以在LinkedList的首部或尾部进行插入或删除操作。这些方法使得LinkedList可被用作堆栈(statck)或者队列(queue)。如图所示:

    在这里插入图片描述

2.2 ArrayList类

底层是动态数组结构,数据查询方便、数据增删改不方便,线程不安全,本质上就是通过创建新的更大的数组,将旧数组内容拷贝到新数组,来实现扩容

当调用无参构造方法来创建ArrayList时,默认容量为10, 当添加的数据容量超过数组容量大小时,会产生一个新数组,新数组容量大小为原数组容量的1.5倍,接着把原数组中的数据复制到新数组中。

常用的方法如下表:

方法名说 明
boolean add(Object o)在列表的末尾顺序添加元素, 起始索引位置从0开始
void add(int index,Object o)在指定的索引位置添加元素。 索引位置必须介于0和列表中元素个数之间
int size()返回列表中的元素个数
Object get(int index)返回指定索引位置处的元素。取出的元素是Object类型,使用前需要进行强制类型转换
boolean contains(Object o)判断列表中是否存在指定元素
boolean remove(Object o)从列表中删除元素
Object remove(int index)从列表中删除指定位置元素, 起始索引位置从0开始

2.3 LinkedList集合类

LinkedList底层数据结构是环形双向链表,查询慢,增删快,线程不安全

(LinkedList实现了接口Deque,可以被当做队列或者栈使用)

它内部封装的是双向链表的数据结构,每个节点是一个Node对象,Node对象中封装的是你要添加的元素,还有一个指向上一个Node对象的引用和一个指向下一个Node对象的引用。

每个节点都应该有3部分内容:

private static class Node<E> {
    Node<E> prev;//前一个节点
    E item;//本节点保存的数据
    Node<E> next;//后一个节点
    //省略其他内容。。。。
}

LinkedList除了包含ArrayList表中的方法外,还包含一些特殊的方法

方法名说 明
void addFirst(Object o)在列表的首部添加元素
void addLast(Object o)在列表的末尾添加元素
Object getFirst()返回列表中的第一个元素
Object getLast()返回列表中的最后一个元素
Object removeFirst()删除并返回列表中的第一个元素
Object removeLast()删除并返回列表中的最后一个元素

2.4 ArrayList与LinkedList性能对比

模拟10w条数据指定插入第一位,然后查询全部循环删除第一位

public class TestList {
    public static void main(String[] args) {
    	List<String> list = new ArrayList<>();
    	//测试ArrayList新增/查询/删除100000次
        testList("ArrayList",list);
        System.out.println("==============分割线===========");
        //测试LinkedList新增/查询/删除100000次
        list = new LinkedList<>();
        testList("LinkedList",list);
    }

    public static void testList(String type,List<String> list) {
        int maxTestCount = 100000;

        //--------测试添加--------
        long start = System.currentTimeMillis();
        for (int i = 0; i < maxTestCount; i++) {
            list.add(0, String.valueOf(i));
        }
        long end = System.currentTimeMillis();
        System.out.println(type+" add cost time :" + (end - start));

        //--------测试查询--------
        start = System.currentTimeMillis();
        for (int i = 0; i < maxTestCount; i++) {
            list.get(i);
        }
        end = System.currentTimeMillis();
        System.out.println(type+" get cost time :" + (end - start));

        //--------测试删除--------
        start = System.currentTimeMillis();
        for (int i = maxTestCount; i > 0; i--) {
            list.remove(0);
        }
        end = System.currentTimeMillis();
        System.out.println(type+" remove cost time :" + (end - start));
    }
}

执行结果如下:

ArrayList add cost time :985
ArrayList get cost time :3
ArrayList remove cost time :958
==============分割线===========
LinkedList add cost time :10
LinkedList get cost time :14157
LinkedList remove cost time :5

从上述结果分析如下:

  • ArrayList插入、删除效率明显低于LinkedList
  • ArrayList查询效率远远高于LinkedList

这个和数据结构有关系:

  • Arraylist 顺序表,用数组的方式实现。查询哪个元素只给出其下标即可,所以才说ArrayList随机访问多的场景比较合适。但是如果删除某个元素比如第 i 个元素,则要将 i 之后的元素都向前移一位以保证顺序表的正确,增加也是一样,要移动多个元素。要多次删除增加的话是很低效的。
  • LinkedList是双向链表。只能从头节点开始逐步查询,没有直接根据下标查询的方法,需要遍历。但是,如果要增加后删除一个元素的话,只需要改变其前后元素的指向即可,不需要像ArrayList那样整体移动,所以才说多用于增删多的场合

3. Map接口

在这里插入图片描述

  • Map集合存储的是键值对
  • Map集合的实现类:HashTable、LinkedHashMap、HashMap、TreeMap

Map接口专门处理键值映射数据的存储,可以根据键实现对值的操作。使用这种方式存储数据的优点是查询指定元素的效率高。

3.1 HashMap集合类

基础了解:

  • 1、键不可以重复,值可以重复;
  • 2、底层使用哈希表实现;
  • 3、线程不安全;
  • 4、允许key为null,但只允许有一条记录为null,value也可以为null,允许多条记录为null;

从结构实现来讲,HashMap是数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的,如下如所示

在这里插入图片描述

3.2 常用方法如下

方法名说 明
Object put(Object key, Object val)以“键-值对”的方式进行存储
Object get (Object key)根据键返回相关联的值,如果不存在指定的键,返回null
Object remove (Object key)删除由指定的键映射的“键-值对”
int size()返回元素个数
Set keySet ()返回键的集合
Collection values ()返回值的集合
boolean containsKey (Object key)如果存在由指定的键映射的“键-值对”,返回true

4. Set接口

Set继承于Collection接口,是一个不允许出现重复元素,并且无序的集合,主要有HashSet和TreeSet两大实现类。

在判断重复元素的时候,Set集合会调用元素的hashCode()和equal()方法来实现。

  • HashSet是哈希表结构,主要利用HashMap的key来存储元素,计算插入元素的hashCode来获取元素在集合中的位置;

  • TreeSet是红黑树结构,每一个元素都是树中的一个节点,插入的元素都会进行排序;

Set集合框架结构:

在这里插入图片描述

4.1 Set常用方法

与List接口一样,Set接口也提供了集合操作的基本方法。

但与List不同的是,Set还提供了equals(Object o)和hashCode(),供其子类重写,以实现对集合中插入重复元素的处理

public interface Set<E> extends Collection<E> {

    // A:添加功能
    boolean add(E e);
    boolean addAll(Collection<? extends E> c);

    // B:删除功能
    boolean remove(Object o);
    boolean removeAll(Collection<?> c);
    void clear();

    // C:长度功能
    int size();

    // D:判断功能
    boolean isEmpty();
    boolean contains(Object o);
    boolean containsAll(Collection<?> c);
    boolean retainAll(Collection<?> c); 

    // E:获取Set集合的迭代器:
    Iterator<E> iterator();

    // F:把集合转换成数组
    Object[] toArray();
    <T> T[] toArray(T[] a);

    //判断元素是否重复,为子类提高重写方法
    boolean equals(Object o);
    int hashCode();
}

4.2 HashSet

HashSet实现Set接口,底层由HashMap来实现,为哈希表结构,新增元素相当于HashMap的key,value默认为一个固定的Object。

当有元素插入的时候,会计算元素的hashCode值,将元素插入到哈希表对应的位置中来;

它继承于AbstractSet,实现了Set, Cloneable, Serializable接口。

  1. HashSet继承AbstractSet类,获得了Set接口大部分的实现,减少了实现此接口所需的工作,实际上是又继承了AbstractCollection类;
  2. HashSet实现了Set接口,获取Set接口的方法,可以自定义具体实现,也可以继承AbstractSet类中的实现;
  3. HashSet实现Cloneable,得到了clone()方法,可以实现克隆功能;
  4. HashSet实现Serializable,表示可以被序列化,通过序列化去传输,典型的应用就是hessian协议

具有如下特点:

  • 不允许出现重复因素;
  • 允许插入Null值;
  • 元素无序(添加顺序和遍历顺序不一致);
  • 线程不安全,若2个线程同时操作HashSet,必须通过代码实现同步;

4.3 HashSet基本操作

HashSet底层由HashMap实现,插入的元素被当做是HashMap的key,根据hashCode值来确定集合中的位置,由于Set集合中并没有下标的概念,所以并没有像List一样提供get()方法。当获取HashSet中某个元素时,只能通过遍历集合的方式进行equals()比较来实现;

5. 迭代器 Iterator

5.1 迭代器概述

在Java中,有很多的数据容器,对于这些的操作有很多的共性。Java采用了迭代器来为各种容器提供了公共的操作接口。这样使得对容器的遍历操作与其具体的底层实现相隔离,达到解耦的效果。

在Iterator接口中定义了三个方法:

在这里插入图片描述

5.2 使用迭代器

迭代器 的基本操作是 next 、hasNext 和 remove。

  • 调用 next() 会返回迭代器的下一个元素,并且更新迭代器的状态。
  • 调用 hasNext() 用于检测集合中是否还有元素。
  • 调用 remove() 将迭代器返回的元素删除。

Iterator 类位于 java.util 包中,使用前需要引入它,语法格式如下:

import java.util.Iterator; // 引入 Iterator 类

Iterator(迭代器)不是一个集合,它是一种用于访问集合的方法,可用于迭代 ListSetMap 等集合。

演示示例如下:

List<String> list = new ArrayList<>();
list.add("aa");
list.add("bb");
list.add("cc");
for (Iterator iter = list.iterator(); iter.hasNext();) {
    String str = iter.next();
    System.out.println(str);
}
/*迭代器用于while循环
 Iterator iter = list.iterator();
 while(iter.hasNext()){
     String str = iter.next();
     System.out.println(str);
 }*/

//遍历Set与遍历List一致	
Set<String> set = new HashSet<>();
set.add("aa");
set.add("bb");
set.add("cc");
for (Iterator<String> iter = set.iterator(); iter.hasNext();) {
    String str = iter.next();
    System.out.println(str);
}
/*迭代器用于while循环
Iterator<String> iter = set.iterator();
while(iter.hasNext()){
    String str = iter.next();
    System.out.println(str);
}*/

//遍历Map
Map<String,Integer> map=new HashMap<>();
map.put("数学",90);
map.put("语文",88);
map.put("英语",90);
//第一种遍历方式
Set<Entry<String,Integer>> set=map.entrySet(); 
//map键值对是存放在节点里面,通过entrySet()获得键值对的集合
for(Iterator<Entry<String,Integer>> it=set.iterator();it.hasNext();)
{
    Entry<String,Integer> s=it.next();
    System.out.println(s.getKey()+"+"+s.getValue());
}
/*迭代器用于while循环
Iterator<Entry<String,Integer>> it=set.iterator();
while(iter.hasNext()){
    Entry<String,Integer> s=it.next();
    System.out.println(s.getKey()+"+"+s.getValue());
}*/
 
//第二种遍历方式,相当于遍历集合*
//相当于建立一个包含所有键的集合,
//再通过Map自带方法map.get(key)返回键对应的值,达到遍历效果
Set<String> set = map.keySet();
for(Iterator<String> iter=set.iterator();it.hasNext();)
{
    String s=iter.next();
    System.out.println(s+"+"+map.get(s));
}
/*迭代器用于while循环
Iterator<String> iter=set.iterator();
while(iter.hasNext()){
    String s=iter.next();
    System.out.println(s+"+"+map.get(s));
}*/

6. 泛型集合

Java中的泛型(generics)是JDK 5中引入的一个新特性,提供编译时类型安全检测机制,允许在编译时检测非法的类型。平常用到的大多数泛型写的程序都和处理集合有关,虽然泛型也可以用在其他地方,但是它的主要目的还是让我们可以写出有类型安全的集合。

在没有泛型功能之前,集合都写成Object类型,我们可以把任何类型的数据放入ArrayList中,就像ArrayList。 使用泛型后,可以指定ArrayList中可存储的对象类型。

泛型带来的好处不只是代码重用了,还包括类型安全,以及更好的可读性

定义泛型类如下:

class Hello<T>{
	//省略其他代码
}

定义泛型方法如下:

public <T>void show(T t){
    //省略其他代码
    System.out.println(t);
}

public <T>T message(T t){
    //省略其他代码
    return t;
}

定义泛型接口如下:

interface Message<T>{
	//省略其他代码
}

7. 泛型通配符

当使用泛型类或者接口时,传递的数据中,泛型类型不确定,可以通过通配符<?>表示。但是一旦使用泛型的通配符后,只能使用Object类中的共性方法,集合中元素自身方法无法使用。

通配符基本使用

泛型的通配符:不知道使用什么类型来接收的时候,此时可以使用?,?表示未知通配符。

此时只能接受数据,不能往该集合中存储数据。

举个例子大家理解使用即可:

public static void main(String[] args) {
    List<Intger> list1 = new ArrayList<Integer>();
    getElement(list1);
    List<String> list2 = new ArrayList<String>();
    getElement(list2);
}
public static void getElement(List<?> list){}
//?代表可以接收任意类型

注意:泛型不存在继承关系 Collection<Object> list = new ArrayList<String>();这种是错误的。

通配符高级使用----受限泛型

之前设置泛型的时候,实际上是可以任意设置的,只要是类就可以设置。但是在JAVA的泛型中可以指定一个泛型的上限下限

泛型的上限

  • 格式类型名称 <? extends 类 > 对象名称
  • 意义只能接收该类型及其子类

泛型的下限

  • 格式类型名称 <? super 类 > 对象名称
  • 意义只能接收该类型及其父类型

比如:现已知Object类,String 类,Number类,Integer类,其中Number是Integer的父类

public static void main(String[] args) {
    List<Integer> list1 = new ArrayList<Integer>();
    List<String> list2 = new ArrayList<String>();
    List<Number> list3 = new ArrayList<Number>();
    List<Object> list4 = new ArrayList<Object>();
    
    getElement1(list1);
    getElement1(list2);//报错
    getElement1(list3);
    getElement1(list4);//报错
  
    getElement2(list1);//报错
    getElement2(list2);//报错
    getElement2(list3);
    getElement2(list4);
  
}
// 泛型的上限:此时的泛型?,必须是Number类型或者Number类型的子类
public static void getElement1(List<? extends Number> list){}
// 泛型的下限:此时的泛型?,必须是Number类型或者Number类型的父类
public static void getElement2(List<? super Number> list){}

8. 包装类

在 Java的设计中提倡一种思想,即一切皆对象。但是从数据类型的划分中,我们知道 Java 中的数据类型分为基本数据类型和引用数据类型,但是基本数据类型怎么能够称为对象呢?于是 Java 为每种基本数据类型分别设计了对应的类,称之为包装类(Wrapper Classes),也有地方称为外覆类或数据类型类。

包装类和基本数据类型的关系如下表所示。

序号基本数据类型包装类
1byteByte
2shortShort
3intInteger
4longLong
5charCharacter
6floatFloat
7doubleDouble
8booleanBoolean

从上表中我们可以看出,除了 Integer 和 Character 定义的名称与基本数据类型定义的名称相差较大外,其它的 6 种类型的名称都是很好掌握的。

8.1 装箱和拆箱

在了解包装类后,下面介绍包装类的装箱与拆箱的概念。其实这两个概念本身并不难理解,基本数据类型转换为包装类的过程称为装箱,例如把 int 包装成 Integer 类的对象;包装类变为基本数据类型的过程称为拆箱,例如把 Integer 类的对象重新简化为 int。

手动实例化一个包装类称为手动拆箱装箱。Java 1.5 版本之前必须手动拆箱装箱,之后可以自动拆箱装箱,也就是在进行基本数据类型和对应的包装类转换时,系统将自动进行装箱及拆箱操作,不用在进行手工操作,为开发者提供了更多的方便。例如:

public class Demo {
    public static void main(String[] args) {
        int m = 500;
        Integer obj = m;  // 自动装箱
        int n = obj;  // 自动拆箱
        System.out.println("n = " + n);
      
        Integer obj1 = 500;
        System.out.println("obj等价于obj1返回结果为" + obj.equals(obj1));
    }
}

运行结果:

n = 500
obj等价于obj1返回结果为true

8.2 包装类的应用

下面我们讲解 8 个包装类的使用,下面是常见的应用场景。

1) 实现 int 和 Integer 的相互转换

可以通过 Integer 类的构造方法将 int 装箱,通过 Integer 类的 intValue 方法将 Integer 拆箱。例如:

public class Demo {
    public static void main(String[] args) {
        int m = 500;
        Integer obj = new Integer(m);  // 手动装箱
        int n = obj.intValue();  // 手动拆箱
        System.out.println("n = " + n);
       
        Integer obj1 = new Integer(500);
        System.out.println("obj等价于obj1的返回结果为" + obj.equals(obj1));
    }
}

运行结果:

n = 500
obj等价于obj1的返回结果为true

一道关于int和Integer的面试题

public class Demo {
    public static void main(String[] args) {
        Integer a = new Integer(3);//创建一个新的对象
        Integer b = 3; // 将3自动装箱成Integer类型
        int c = 3;
        System.out.println(a == b); // false 两个引用没有引用同一对象
        System.out.println(a == c); // true a自动拆箱成int类型再和c比较
        
        Integer f1 = 100, f2 = 100, f3 = 150, f4 = 150;
        System.out.println(f1 == f2);  //true   
        System.out.println(f3 == f4);  //false
    }
}

首先需要注意的是f1、f2、f3、f4四个变量都是Integer对象引用,所以下面的==运算比较的不是值而是引用。

装箱的本质是什么呢?当我们给一个Integer对象赋一个int值的时候,会调用Integer类的静态方法valueOf,如果看看valueOf的源代码就知道发生了什么。

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

IntegerCache是Integer的内部类,其代码如下所示:

private static class IntegerCache {
    static final int low = -128;
    static final int high;
    static final Integer cache[];

    static {
        // high value may be configured by property
        int h = 127;
        String integerCacheHighPropValue =
            VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
        if (integerCacheHighPropValue != null) {
            try {
                int i = parseInt(integerCacheHighPropValue);
                i = Math.max(i, 127);
                // Maximum array size is Integer.MAX_VALUE
                h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
            } catch( NumberFormatException nfe) {
                // If the property cannot be parsed into an int, ignore it.
            }
        }
        high = h;

        cache = new Integer[(high - low) + 1];
        int j = low;
        for(int k = 0; k < cache.length; k++)
            cache[k] = new Integer(j++);

        // range [-128, 127] must be interned (JLS7 5.1.7)
        assert IntegerCache.high >= 127;
    }

    private IntegerCache() {}
}

简单的说,如果整型字面量的值在-128到127之间,那么不会new新的Integer对象,而是直接引用常量池中的Integer对象,所以上面的面试题中f1==f2的结果是true,而f3==f4的结果是false

2) 将字符串转换为数值类型

在 Integer类中分别提供了以下方法:

Integer 类(String 转 int 型)

int parseInt(String s);

注意:使用上面的方法时,字符串中的数据必须由数字组成,否则转换时会出现程序错误。

下面为字符串转换为数值类型的例子:

public class Demo {
    public static void main(String[] args) {
        String str1 = "30";
        String str2 = "30.3";
        // 将字符串变为int型
        int x = Integer.parseInt(str1);
        // 将字符串变为float型
        float f = Float.parseFloat(str2);
        System.out.println("x = " + x + ";f = " + f);
    }
}

其他的包装类也都一样通过parseXXX方法转换

3) 将整数转换为字符串

Integer 类有一个静态的 toString() 方法,可以将整数转换为字符串。例如:

public class Demo {
    public static void main(String[] args) {
        int m = 500;
        String s = Integer.toString(m);
        System.out.println("s = " + s);
    }
}

9. Collections

9.1 常用功能

  • java.utils.Collections是集合工具类,用来对集合进行操作。部分方法如下:
  • public static <T> boolean addAll(Collection<T> c, T... elements):往集合中添加一些元素。
  • public static void shuffle(List<?> list) 打乱顺序:打乱集合顺序。
  • public static <T> void sort(List<T> list):将集合中元素按照默认规则排序。
  • public static <T> void sort(List<T> list,Comparator<? super T> ):将集合中元素按照指定规则排序。

示例代码如下:

public class CollectionsDemo {
	public static void main(String[] args) {
		//创建集合
		List<String> list = new ArrayList<String>();
		//增加10个不同单词
		//list.add("this");
		//list.add("is");
		//list.add("collection");
		//list.add("test");
		//list.add("and");
		//list.add("we");
		//list.add("can");
		//list.add("learn");
		//list.add("how");
		//list.add("to");
		//采用工具类 完成 往集合中添加元素  
        Collections.addAll(list, "this","is","collection","test","and","we",
                           "can","learn","how","to");
		//打印输出集合中最大元素和最小元素   
		String strMax = (String) Collections.max(list);
		String strMin = (String) Collections.min(list);
		System.out.println("最大值:"+strMax);
		System.out.println("最小值:"+strMin);
		//按升序打印输出集合中所有元素   
		Collections.sort(list);
		System.out.println("集合升序");
		for(int i=0;i<list.size();i++)
		{
			System.out.println(list.get(i));
		}
		System.out.println(Collections.binarySearch(list, "this"));
		//按降序打印输出集合中所有元素
		Collections.reverse(list);
		System.out.println("集合降序");
		for(int i=0;i<list.size();i++)
		{
			System.out.println(list.get(i));
		}

	}

}

代码演示之后 ,发现我们的集合按照顺序进行了排列,可是这样的顺序是采用默认的顺序,如果想要指定顺序那该怎么办呢?

我们发现还有个方法没有讲,public static <T> void sort(List<T> list,Comparator<? super T> ):将集合中元素按照指定规则排序。接下来讲解一下指定规则的排列。

9.2 Comparator比较器

我们还是先研究这个方法

public static <T> void sort(List<T> list):将集合中元素按照默认规则排序。

public class CollectionsDemo2 {
    public static void main(String[] args) {
        ArrayList<String>  list = new ArrayList<String>();
        list.add("cba");
        list.add("aba");
        list.add("sba");
        list.add("nba");
        //排序方法
        Collections.sort(list);
        System.out.println(list);
    }
}

结果:

[aba, cba, nba, sba]

我们使用的是默认的规则完成字符串的排序,那么默认规则是怎么定义出来的呢?

说到排序了,简单的说就是两个对象之间比较大小,那么在JAVA中提供了两种比较实现的方式

  • 一种是比较死板的采用java.lang.Comparable接口去实现
  • 一种是灵活的当我需要做排序的时候在去选择的java.util.Comparator接口完成。

那么我们采用的public static <T> void sort(List<T> list)这个方法完成的排序,实际上要求了被排序的类型需要实现Comparable接口完成比较的功能,在String类型上如下:

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {

String类实现了这个接口,并完成了比较规则的定义,但是这样就把这种规则写死了,那比如我想要字符串按照第一个字符降序排列,那么这样就要修改String的源代码,这是不可能的了,那么这个时候我们可以使用

public static <T> void sort(List<T> list,Comparator<? super T> )方法灵活的完成,这个里面就涉及到了Comparator这个接口,位于位于java.util包下,排序是comparator能实现的功能之一,该接口代表一个比较器,比较器具有可比性!顾名思义就是做排序的,通俗地讲需要比较两个对象谁排在前谁排在后,那么比较的方法就是:

  • public int compare(String o1, String o2):比较其两个参数的顺序。

    两个对象比较的结果有三种:大于,等于,小于。

    如果要按照升序排序,
    则o1 小于o2,返回(负数),相等返回0,01大于02返回(正数)
    如果要按照降序排序
    则o1 小于o2,返回(正数),相等返回0,01大于02返回(负数)

操作如下:

public class CollectionsDemo3 {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<String>();
        list.add("cba");
        list.add("aba");
        list.add("sba");
        list.add("nba");
        //排序方法  按照第一个单词的降序
        Collections.sort(list, new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                return o2.charAt(0) - o1.charAt(0);
            }
        });
        System.out.println(list);
    }
}

结果如下:

[sba, nba, cba, aba]

9.3 简述Comparable和Comparator两个接口的区别

Comparable

强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序,类的compareTo方法被称为它的自然比较方法。只能在类中实现compareTo()一次,不能经常修改类的代码实现自己想要的排序。实现此接口的对象列表(和数组)可以通过Collections.sort(和Arrays.sort)进行自动排序,对象可以用作有序映射中的键或有序集合中的元素,无需指定比较器。

Comparator

强行对某个对象进行整体排序。可以将Comparator 传递给sort方法(如Collections.sort或 Arrays.sort),从而允许在排序顺序上实现精确控制。还可以使用Comparator来控制某些数据结构(如有序set或有序映射)的顺序,或者为那些没有自然顺序的对象collection提供排序。

9.4 案例

创建一个学生类,存储到ArrayList集合中完成指定排序操作。

Student 初始类

public class Student{
    private String name;
    private int age;

    public Student() {
    }

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" +
               "name='" + name + '\'' +
               ", age=" + age +
               '}';
    }
}

测试类:

public class Demo {
    public static void main(String[] args) {
        // 创建四个学生对象 存储到集合中
        ArrayList<Student> list = new ArrayList<Student>();

        list.add(new Student("rose",18));
        list.add(new Student("jack",16));
        list.add(new Student("abc",16));
        list.add(new Student("ace",17));
        list.add(new Student("mark",16));

        /*
          让学生 按照年龄排序 升序
         */
		//Collections.sort(list);
        //要求 该list中元素类型  必须实现比较器Comparable接口
        for (Student student : list) {
            System.out.println(student);
        }
    }
}

发现,当我们调用Collections.sort()方法的时候 程序报错了。

原因:如果想要集合中的元素完成排序,那么必须要实现比较器Comparable接口。

重写compareTo(参数)方法来定义对象间的比较规则。

该方法要求返回一个整数,这个整数不关心具体的值,而是关注取值范围。

当返回值>0时,表示当前对象比参数给定的对象大。

当返回值<0时,表示当前对象比参数给定的对象小。

当返回值=0时,表示当前对象和参数给定的对象相等。

于是我们就完成了Student类的一个实现,如下:

public class Student implements Comparable<Student>{
    ....
    @Override
    public int compareTo(Student o) {
        //return o.age-this.age //降序
        return this.age-o.age;//升序
        //年龄升序 年龄如果相同,则按照姓名首字母升序
        //return this.age - o.age == 0 ? 
			//this.name.charAt(0) - o.name.charAt(0) : this.age - o.age;
    }
}

再次测试,代码就OK 了效果如下:

Student{name='jack', age=16}
Student{name='abc', age=16}
Student{name='mark', age=16}
Student{name='ace', age=17}
Student{name='rose', age=18}

9.5 扩展

如果在使用的时候,想要独立的定义规则去使用 可以采用Collections.sort(List list,Comparetor<T> c)方式,自己定义规则:

Collections.sort(list, new Comparator<Student>() {
    @Override
    public int compare(Student o1, Student o2) {
        //return o1.getAge()-o2.getAge();//以学生的年龄升序
        return o2.getAge()-o1.getAge();//以学生的年龄降序
    }
});

效果:

Student{name='rose', age=18}
Student{name='ace', age=17}
Student{name='jack', age=16}
Student{name='abc', age=16}
Student{name='mark', age=16}

如果想要规则更多一些,可以参考下面代码:

Collections.sort(list, new Comparator<Student>() {
            @Override
            public int compare(Student o1, Student o2) {
                // 年龄降序
                int result = o2.getAge()-o1.getAge();//年龄降序

                if(result==0){//第一个规则判断完了 下一个规则 姓名的首字母 升序
                    result = o1.getName().charAt(0)-o2.getName().charAt(0);
                }

                return result;
            }
        });

以上代码使用lambda表达式第一种写法简化如下:

Collections.sort(list, (o1,o2)-> {
    // 年龄降序 年龄如果相同 按姓名的首字母升序
  o2.getAge()-o1.getAge()==0?o1.getName().charAt(0)-o2.getName().charAt(0):o2.getAge()-o1.getAge());

以上代码使用lambda表达式第二种写法简化如下:

public class Demo {
    public static void main(String[] args) {
        // 创建四个学生对象 存储到集合中
        ArrayList<Student> list = new ArrayList<Student>();

        list.add(new Student("rose",18));
        list.add(new Student("jack",16));
        list.add(new Student("abc",16));
        list.add(new Student("ace",17));
        list.add(new Student("mark",16));

        /*
          年龄降序 年龄如果相同 按姓名的首字母升序
         */
        //lambda第二种编写方式 使用方法引用
		Collections.sort(list,Demo::compare);
        //要求 该list中元素类型  必须实现比较器Comparable接口
        for (Student student : list) {
            System.out.println(student);
        }
    }
    //使用方法引用
	public static int compare(Student o1,Student o2) {
		return  o2.getAge()-o1.getAge()==0 ? o1.getName().charAt(0)-o2.getName().charAt(0) : o2.getAge()-o1.getAge();
	}
}

效果如下:

Student{name='rose', age=18}
Student{name='ace', age=17}
Student{name='abc', age=16}
Student{name='jack', age=16}
Student{name='mark', age=16}

10. 备注

Object 类是所有类的父类,其 equals 方法比较的是两个对象的引用指向的地址,hashCode 是一个本地方法,返回的是对象地址值。他们都是通过比较地址来比较对象是否相等的

使用HashMap集合,存储自定义的对象。 如果自定义的对象作为键(键不能重复),需要重写自定义类型的hashCode()和equals()方法。

主要原因是:当我们用自定义的类的作为HashMap的key时,如何判断这相同的类对象的唯一性,这涉及到HashMap的判断机制,先比较key的hashCode(),当哈希值相同时,再比较key的equals(),这两个是配套的。

所以重写 hashCode 方法是为了让我们能够正常使用 HashMap 等集合类,因为 HashMap 判断对象是否相等既要比较 hashCode 又要使用 equals 比较。而这样的实现是为了提高 HashMap 的效率。

重写一个类的hashcode()方法有以下注意事项:

  1. 若一个类重写了equals()方法,那么就应当重写hashcode()方法。
  2. 若两个对象的equals()方法比较为true,那么它们应当具有相同的hashcode值。
  3. 对于同一个对象而言,在内容没有发生改变的情况下,多次调用hashCode()方法应当总是返回相同的值。
  4. 对于两个对象equals()比较为false的,并不要求其hashCode值一定不同,但应尽量保证不同,这样可以提高散列表性能。

重写可以使用开发工具生成。

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值