目录

1.比较器

1.1 作用

java中定义的运算符,大多数只适用于基本数据类型。因为引用数据类型的对象不能直接使用运算符进行比较,所以java提供了比较器(内部还是对基本数据类型进行的比较)。

1.2 两种实现对象排序的方式

java.lang.Comparable:自然排序
java.lang.Comparator:定制排序

1.2.1 两者区别

Comparable接口的比较规则是在实现类中重写的,此类中所有的对象都可以使用;
Comparator接口的比较规则是在排序时临时定义的;

1.3 Comparable接口

1.3.1 概述

  1. Comparable接口中有一个抽象方法public int compareTo(T o);,像String,包装类都重写了compareTo方法,用于对象之间的比较。
  2. 如果想要对自定义类的对象进行默认排序,必须实现Comparable接口,然后重写compareTo方法,在compareTo方法中进行属性的比较;
  3. 实现Comparable接口的对象列表(和数组)可以通过 Collections.sortArrays.sort进行自动排序;
  4. 重写规则得到的结果是升序,如果想要降序,取相反数

1.3.2 重写compareTo(T o)方法的规则

如果当前对象大于形参对象o,返回正整数;
如果当前对象小于形参对象o,返回负整数;
如果两者相等,返回0;

1.3.3 缺点

只能在类中重写compareTo方法,类中所有的对象使用相同比较方式,不能在具体对象中重写,有局限性。

1.3.4 实例

方式1:直接进行属性的比较
方式2:可以调用相应的包装类中的compare方法

    @Override
    public int compareTo(Object o) {
        if (o instanceof Person) {
            Person o1 = (Person) o;
            return this.age > o1.age ? 1 : (this.age == o1.age) ? 0 : -1; 
            return Integer.compare(this.age, o1.age);
        }
        throw new RuntimeException("传入类型不正确");
    }

1.4 Comparator接口

1.4.1 概述

  1. 如果类中没有实现Comparable接口,或者类中实现了Comparable接口但不适合当前操作且不想修改类中代码,可以考虑使用 Comparator 的对象来排序。
  2. 一般使用(匿名)内部类,需要重写Comparator 接口中的compare(Object o1,Object o2)方法,重写规则与compareTo()一致。
  3. 创建匿名内部类当作排序方法的形参传入,排序规则只可以使用一次。

1.4.2 实例

p是一个Person类型的数组

// 简化前:满足重写规则,默认为升序,如果想要降序,取相反数
        Collections.sort(characters, new Comparator<Character>() {
            @Override
            public int compare(Character o1, Character o2) {
                if (o1 - o2 > 0) {
                    return 1;
                } else if (o1 - o2 < 0) {
                    return -1;
                } else {
                    return 0;
                }
            }
        });
// 简化后:因为重写规则中o1小于o2时,需要返回负数,而此时o1-o2也是负数;同理o1大于或是等于o2时,返回值也和o1-o2的结果一致,所以就使用o1-o2的结果来充当返回值。
        Arrays.sort(p, new Comparator<Person>() {
            @Override
            public int compare(Person o1, Person o2) {
                return o1.getAge() - o2.getAge();
            }
        });

2.System类、Math类、BigInteger与BigDecimal

3.枚举类

3.1 概述

  1. 如果一个类只有有限个确定的对象,那么可以使用枚举类。
  2. 如果需要定义一组常量时,强烈建议使用枚举类。
  3. 若枚举只有一个对象, 则可以作为一种线程安全的单例模式的实现方式。

3.1.1 一些例子

星期:Monday(星期一)、…、Sunday(星期天)
性别:Man(男)、Woman(女)
季节:Spring(春节)…Winter(冬天)

3.2 jdk1.5之前的定义方式

class Season{
//    1. 声明枚举类的属性:private final
    private final String name;
    private final int age;
    
//    2. 私有化构造器,并在构造器中初始化对象的属性
    private Season(String name,int age){
        this.name = name;
        this.age = age;
    }
//    3. 创建本类的对象:static final
    public static final Season SPRING  = new Season("春天",1);
    public static final Season SUMMER  = new Season("夏天",2);
    public static final Season AUTUMN  = new Season("秋天",3);
    public static final Season WINTER  = new Season("冬天",4);
//    4. 其余诉求:提供属性的getter方法
    public String getName() {
        return name;
    }
    public int getAge() {
        return age;
    }

//    5. 其余诉求: 重写同toString方法
    @Override
    public String toString() {
        return "Season{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

3.3 jdk1.5新增的定义方式–enum关键字

3.3.1 细节

  1. 枚举类中创建的常量对象放到类的最上边,仅由常量名和参数列表构成,其余相同修饰信息都可以省略(public static final );
  2. 枚举类的常量对象之间用",“隔开,末尾对象”;"结束;
  3. 属性、私有化构造器、getter方法和上面的定义方式一致;
  4. enum类继承于java.lang.Enum类,所以不能再继承于其他类;
  5. Enum类中重写了toString()方法,返回当前枚举对象的常量名;
  6. JDK 1.5 中可以在 switch 表达式中使用Enum定义的枚举类的对象作为表达式, case 子句可以直接使用枚举值的名字, 无需添加枚举类作为限定。

3.3.2 代码

enum Season1{
//    1. 枚举类对象:放在类的最前边,仅由对象名和参数列表构成
    SPRING("春天",1),
    SUMMER("夏天",2),
    AUTUMN("秋天",3),
    WINTER("冬天",4);
//    2. 声明枚举类的属性:private final
    private final String name;
    private final int age;

//    3. 私有化构造器,并在构造器中初始化对象的属性
    private Season1(String name,int age){
        this.name = name;
        this.age = age;
    }
//     4. 其余诉求:提供属性的getter方法
    public String getName() {
        return name;
    }
    public int getAge() {
        return age;
    }
}

如果没有属性,那么空参构造器可以省略,直接写对象名即可。

enum Season1{
    SPRING,
    SUMMER,
    AUTUMN,
    WINTER;
}

3.4 Enum类的主要方法

  1. values()方法:返回枚举类型的对象数组。该方法可以很方便地遍历所有的枚举值。
  2. valueOf(String str):可以把一个字符串转为对应的枚举类对象。要求字符串必须是枚举类对象的“名字”。如不是,会有运行时异常:IllegalArgumentException。可以用来查找枚举类中的对象。
  3. toString():返回当前枚举类对象常量的名称
enum Season1{
    SPRING,
    SUMMER,
    AUTUMN,
    WINTER;
}

 Season1[] values = Season1.values();
 for (int i = 0; i < values.length; i++) {
     System.out.println(values[i]);
 }
System.out.println("--------------------------------------"); 
Season1 season1 = Season1.valueOf("SPRING");
System.out.println(season1);
SPRING
SUMMER
AUTUMN
WINTER
-----------------------------
SPRING

3.5 枚举类实现接口

3.5.1 细节

  1. 和普通 Java 类一样,枚举类可以实现一个或多个接口;
  2. 若每个枚举值在调用实现的接口方法呈现相同的行为方式,则只要统一实现该方法即可;
  3. 若需要每个枚举值在调用实现的接口方法呈现出不同的行为方式, 则可以让每个枚举值分别来实现该方法;

3.5.2 统一实现方法

interface test{
    void show();
}
enum Season1 implements test{
    SPRING,
    SUMMER,
    AUTUMN,
    WINTER;

    @Override
    public void show() {
        System.out.println("季节");
    }
}

3.5.3 每个枚举值分别来实现方法

interface test{
    void show();
}
enum Season1 implements test{
    SPRING{
        @Override
        public void show() {
            System.out.println("春");
        }
    },
    SUMMER {
        @Override
        public void show() {
            System.out.println("夏");
        }
    },
    AUTUMN {
        @Override
        public void show() {
            System.out.println("秋");
        }
    },
    WINTER {
        @Override
        public void show() {
            System.out.println("冬");
        }
    };

}

4.集合

4.1 概述

4.1.1 定义

集合是对多个数据进行存储和操作的结构,是java提供的一种容器。
Map双列集合与Collection单列集合并列存在。

4.1.2 数组与集合的区别

  1. 数组初始化之后,长度就不可更改;
  2. 数组的方法和属性较少,不便于进行数据的删除,插入等操作,且效率低;
  3. 无法直接获取数组中的元素个数;
  4. 数组是有序、可重复的;
  5. 数组只能存放单一数据类型的数据;

4.1.3 线程安全和线程不安全的集合

只有Vector和Stack、Hashtable和Properties这两对父子类是线程不安全的。

5.Collection集合

在这里插入图片描述

5.1 细节

  1. 一般往集合中添加元素时,需要重写类的equal()方法,因为contains()和remove()这些方法都需要调用equal()方法比较对象是否相等。
  2. Collection集合中存放的都是引用数据类型,基本数据类型会自动装箱。
  3. Collection集合元素可以是 null。

5.2 常用方法

  1. 添加(尾部添加)
    add(Objec tobj)
    addAll(Collection coll)
  2. 获取有效元素的个数
    int size()
  3. 清空集合
    void clear()
  4. 是否是空集合
    boolean isEmpty()
  5. 是否包含某个元素
    boolean contains(Object obj):是通过元素的equals方法来判断是否是同一个对象
    boolean containsAll(Collection c):也是调用元素的equals方法来比较的。与次序无关,只要内容相同即可。
  6. 删除
    boolean remove(Object obj) :通过元素的equals方法判断是否是要删除的那个元素。只会删除找到的第一个元素
    boolean removeAll(Collection coll):取当前集合的差集
  7. 取两个集合的交集
    boolean retainAll(Collection c):把交集的结果存在当前集合中,不影响c
  8. 集合是否相等,对于List集合,因为有序,所以必须挨个比较集合中的元素是否相等;对于set集合,无序,只要两个集合中元素相同即可。
    boolean equals(Object obj)
  9. 转成对象数组
    Object[] toArray()
  10. 获取集合对象的哈希值
    hashCode()
  11. 遍历
    iterator():返回迭代器对象,用于集合遍历

5.3 数组转集合

使用Arrays中的静态方法asList(T… a),参数是可变参数序列,也可以是数组
返回的List集合不能进行更改集合长度的操作,例如add和remove,不然直接抛异常

  1. 如果是基本数据类型的匿名数组,那么会被当作是一个元素
        List ints = Arrays.asList(new int[]{1, 2, 3});
        System.out.println(ints.size()); // 1
  1. 引用数据类型的匿名数组或者可变参数序列,传入几个就算几个
        List ints = Arrays.asList(new String[]{"1", "2", "3"});
        System.out.println(ints.size()); // 3
        
     	List int1 = Arrays.asList(1,2,3,4);
        System.out.println(int1.size()); // 4

5.4 迭代器iterator

5.4.1 作用

用于遍历 Collection 集合中的元素。

5.4.2 方法

  1. hasNext() 判断当前集合是否还有下一个元素
  2. next() 指针后移,并将指针指向的元素返回
  3. remove() 移除指针指向的集合元素,如果还未调用next()或在上一次调用 next 方法之后已经调用了 remove 方法,再调用remove都会报IllegalStateException。

5.4.3 细节

  1. iterator迭代器只用于遍历Collection集合,Iterator本身并不提供承装对象的能力。如果需要创建Iterator 对象,则必须有一个被迭代的集合。
  2. 集合对象每次调用iterator()方法都得到一个全新的迭代器对象,默认游标都在集合的第一个元素之前。
  3. 不经判断,直接调用it.next()可能会抛出NoSuchElementException异常。

5.4.4 实例

        List ints = new ArrayList();
        ints.add(1);
        ints.add(2);
        ints.add(3);
        ints.add(4);
        Iterator it = ints.iterator();
        while (it.hasNext()){
            Object next = it.next();
            if(next.equals(3)){
                it.remove();
            }
        }
        System.out.println(ints); // [1, 2, 4]

5.5 foreach

5.5.1 作用

  1. 用来遍历数组或者Collection集合,底层是通过iterator迭代器实现的。
  2. 只能遍历元素,在遍历过程中不能修改数组或集合中的元素

5.5.2 实例

        int[] arr = new int[]{1, 2, 3};
        for (int n : arr) {
            System.out.println(n);
        }

5.6 List接口(动态数组)

List是ArrayList、LinkedList、Vector的父接口,所以它们实现了这些方法。
List除了从Collection集合继承的方法外,List 集合里添加了一些根据索引来操作集合元素的方法。

  1. void add(int index, Object ele):在index位置插入ele元素
  2. boolean addAll(int index, Collection eles):从index位置开始将eles中的所有元素添加进来
  3. Object get(int index):获取指定index位置的元素
  4. int indexOf(Object obj):返回obj在集合中首次出现的位置,不存在返回-1
  5. int lastIndexOf(Object obj):返回obj在当前集合中末次出现的位置
  6. Object remove(int index):移除指定index位置的元素,并返回此元素
  7. Object set(int index, Object ele):设置指定index位置的元素为ele
  8. List subList(int fromIndex, int toIndex):返回从[fromIndex,toIndex)位置的子集合

5.6.1 常用方法

  1. 增(尾部):add(Object obj)
  2. 删:remove(int index) / remove(Object obj)
  3. 改:set(int index, Object ele)
  4. 查:get(int index)
  5. 插:add(int index, Object ele)
  6. 长度:size()
  7. 遍历:① Iterator迭代器方式 ② 增强for循环 ③ 普通的循环
        ArrayList a = new ArrayList();
        a.add(1);
        a.add(2);
        a.add(3);
        a.add(4);
        a.add(5);
        for (int i = 0; i < a.size(); i++) {
            System.out.println(a.get(i));
        }
        for (Object i : a) {
            System.out.println(i);
        }

5.6.2 细节

List中添加的对象所在类需要重写equals()方法,虽然List可以插入相同的元素,插入时不需要使用equals()方法比较,但是remove()和contains()方法需要使用equals()方法比较。

5.7 ArrayList

5.7.1 特点

  1. 底层是通过Object[] elementData数组来存储数据,查找修改快,插入和删除慢;
  2. 线程不安全,效率高;
  3. 如果确定集合元素个数,开发中一般使用public ArrayList(int initialCapacity)带参构造器来创建集合对象,避免了扩容造成的系统开销;
  4. 可以使用构造器public ArrayList(Collection<? extends E> c)其余Collection实现类转为ArrayList集合;

5.7.2 jdk1.8 源码解析

  1. 使用空参构造器创建ArrayList后,此时集合的容量为0,只有第一次调用add()/addAll()方法时,才会分配空间。如果第一次添加的元素个数小于等于10,那么集合容量定为10;如果大于10,则集合容量定为所添加的元素个数。之后的扩容,都是将容量扩为当前的1.5倍。
  2. 如果使用带参构造器 public ArrayList(int initialCapacity),会直接创建一个指定容量的数组this.elementData = new Object[initialCapacity];,首次创建并不会扩容。
    在这里插入图片描述

5.8 LinkedList

5.8.1 特点

  1. 底层数据结构是双向链表,插入删除快,查找修改慢;
  2. 线程不安全,效率高;

5.8.2 新增方法

  1. void addFirst(Object obj) 在链表头部插入一个新节点,相当于add(0,obj);
  2. void addLast(Object obj) 在链表尾部插入一个新节点,相当于add(obj);
  3. Object getFirst() 得到表头元素,相当于get(0)
  4. Object getLast() 得到表尾元素,相当于get(list.size()-1)
  5. Object removeFirst() 删除表头元素,相当于remove()或者remove(0)
  6. Object removeLast() 删除表尾元素,相当于remove(list.size()-1)

5.8.3 源码分析

  1. 通过空参构造器创建 public LinkedList() { }
  2. 添加元素
    public boolean add(E e) {
        linkLast(e);
        return true;
    }
    将元素添加到链表末尾
    void linkLast(E e) {
	    final Node<E> l = last; // 获取当前末尾元素
	    final Node<E> newNode = new Node<>(l, e, null);// 新建节点
	    last = newNode; // 将新建节点作为链表最后一个元素
	    if (l == null)
	        first = newNode;
	    else
	        l.next = newNode;
	    size++;
	    modCount++;
	}
	// 链表结构
   private static class Node<E> {
      E item;
      Node<E> next;
      Node<E> prev;

      Node(Node<E> prev, E element, Node<E> next) {
          this.item = element;
          this.next = next;
          this.prev = prev;
      }
  }

5.9 Vector

5.9.1 特点

  1. 底层是通过数组来存储数据;
  2. 线程安全,效率低;
  3. 大多数操作与ArrayList相同,区别之处在于Vector是线程安全的。现在一般不使用Vector;

5.9.2 源码分析

  1. 空参构造创建对象,容量设为10; public Vector() { this(10); }
  2. 默认扩容为原来容量的2倍;

5.10 面试题–remove(int index)和remove(Object obj)的选择

因为List中有两个构成重载的方法remove(int index)和remove(Object obj),如果直接传入一个整数,那么选择的是remove(int index);如果传入一个Integer类型的对象,那么选择的是remove(Object obj)。

@Test
public void testListRemove() {
	List list = new ArrayList();
	list.add(1);
	list.add(2);
	list.add(3);
	updateList(list);
	System.out.println(list);//[1, 2]
}
private static void updateList(List list) {
	list.remove(2);
}

5.11 ArrayList/LinkedList/Vector的异同

相同点:都是List接口的实现类,存放的元素是有序可重复的。
不同点:
java学习笔记4 — 比较器、枚举类、集合-小白菜博客
如果在尾部添加元素,ArrayList效率要高于LinkedList,因为数组具有随机访问性,ArrayList可以直接定位到尾部元素的位置,且不需要移动其它元素,时间复杂度为O(1);而链表不具备随机访问性,LinkedList需要从头部往后依次查找,时间复杂度为O(n);

5.12 Vector和ArrayList的最大区别

Vector和ArrayList的最大区别是Vector是线程安全的,ArrayList不是。

5.12 Set接口

5.12.1 概述

  1. Set集合无序,不可重复。

无序:不等于随机性,添加元素时不会按照数组索引递增的顺序存放,而是根据hash值来确定存放位置。而且不能说遍历顺序与存放顺序不一致,因为LinkedHashSet遍历顺序与存放顺序一致。
不可重复:相同的元素只能添加一个,具体比较方式看实现类。

  1. Set接口是Collection的子接口,set接口没有提供额外的方法。
  2. 去重HashSet和LinkedHashSet中存放的对象所在类需要重写hashCode()和equals()方法;TreeSet可以不用重写hashCode()和equals()方法,但是一定要实现比较器接口。

5.13 HashSet

5.13.1 jdk1.8 源码

  1. 对象创建:创建一个HashMap对象
public HashSet() {
    map = new HashMap<>();
}
  1. 添加元素过程
    向HashSet中添加元素a时,首先要计算元素a的哈希值,通过哈希值得到在数组中的索引,如果索引位置没有元素,添加成功
    如果索引位置有元素,比较元素a与索引位置其他元素的哈希值(同一链表上的元素):
    如果哈希值不同,添加成功
    如果哈希值相同,则使用元素a的equals()方法进行比较,如果不相等,添加成功;

5.13.2 细节

  1. HashSet底层是数组+链表+红黑树实现的(JDK1.8之后)。
  2. 对于存放在Set容器中的对象,对应的类一定要重写equals()和hashCode(Object obj)方法,以实现对象相等规则。即:“相等的对象必须具有相等的散列码”。
  3. HashSet 线程不安全。

5.13.3 重写hashCode()和equals()

  1. 因为Object中的hashCode()返回的是对象的内存地址,而集合添加元素时关注的是对象的内容是否相等(内容相同的对象内存地址不一定一样),所以根据对象的属性重写hashCode(),保证相等的对象必须具有相等的哈希值(哈希值用来获取数组索引)。
  2. 在程序运行时,同一个对象多次调用 hashCode() 方法应该返回相同的值。(唯一性
  3. 当两个对象的 equals() 方法比较返回 true 时,这两个对象的 hashCode() 方法的返回值也应相等。(对等性
  4. 对象中用作 equals() 方法比较的属性,都应该用来计算 hashCode 值。保证对等性的实现
  5. 一般使用编辑器自动生成的代码即可。

5.13.4 为什么用Eclipse/IDEA重写hashCode方法中,使用31这个数字?

    public static int hashCode(Object a[]) {
        if (a == null)
            return 0;

        int result = 1;

        for (Object element : a)
            result = 31 * result + (element == null ? 0 : element.hashCode());

        return result;
    }
  1. 选择系数的时候要选择尽量大的系数。因为如果计算出来的hash地址越大,所谓的“冲突”就越少,查找起来效率也会提高。(减少冲突)
  2. 31只占用5bits,相乘造成数据溢出的概率较小。
  3. 31可以 由i*31== (i<<5)-1来表示,现在很多虚拟机里面都有做相关优化。(提高算法效率)
  4. 31是一个素数,素数作用就是如果我用一个数字来乘以这个素数,那么最终出来的结果只能被素数本身和被乘数还有1来整除!(减少冲突)

5.14 LinkedHashSet

5.14.1 细节

  1. LinkedHashSet是HashSet的子类。
  2. LinkedHashSet根据元素的 hashCode 值来决定元素的存储位置,但它同时使用双向链表维护元素的次序,保证遍历顺序与存放的顺序一致。
  3. LinkedHashSet 不允许集合元素重复。

5.14.2 底层结构

在这里插入图片描述

5.15 TreeSet

5.15.1 细节

  1. TreeSet 可以确保集合元素处于排序状态,有两种排序方法:自然排序和定制排序。
  2. 同一个TreeSet集合只能添加相同数据类型的对象。
  3. 对于 TreeSet 集合而言,它判断两个对象是否相等的唯一标准是:两个对象通过 compareTo(Object obj) 或者compare(Object o1, Object o2)方法比较的返回值是否为0。如果两个对象比较返回值是0,那么后者不会被添加到集合中(Set不可重复)

5.15.2 自然排序

添加对象的所在类必须实现Comparable接口,重写comparTo()方法

根据年龄排序
      TreeSet t = new TreeSet();
        t.add(new Person("jack",22));
        t.add(new Person("merry",17));
        t.add(new Person("json",5));
        t.add(new Person("luse",35));
        t.add(new Person("facker",35)); // 不会添加到集合中
        for (Object o: t) {
            System.out.println(o);
        }
Person{name='json', age=5}
Person{name='merry', age=17}
Person{name='jack', age=22}
Person{name='luse', age=35}

5.15.3 定制排序

  1. TreeSet的自然排序要求元素所属的类实现Comparable接口,如果元素所属的类没有实现Comparable接口,或不希望按照升序(默认情况)的方式排列元素或希望按照其它属性大小进行排序,则考虑使用定制排序。定制排序,通过Comparator接口来实现。需要重写compare(T o1,T o2)方法。
  2. 要实现定制排序,需要将实现Comparator接口的实例作为形参传递给TreeSet的构造器。
根据名字排序
        TreeSet t = new TreeSet(new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                return ((Person)o1).getName().compareTo(((Person)o2).getName());
            }
        });
        t.add(new Person("jack",22));
        t.add(new Person("merry",17));
        t.add(new Person("hson",5));
        t.add(new Person("luse",35));
        t.add(new Person("facker",35));
        for (Object o: t) {
            System.out.println(o);
        }
Person{name='facker', age=35}
Person{name='hson', age=5}
Person{name='jack', age=22}
Person{name='luse', age=35}
Person{name='merry', age=17}

5.16 练习

5.16.1 去除List中的重复数据

思路:通过set集合来实现

  1. 将list集合中的所有元素添加到Set集合中;
  2. 将Set集合作为List集合构造器的形参创建List集合实例。
        List list = new ArrayList();
        list.add(1);
        list.add(1);
        list.add(2);
        list.add(2);
        list.add(3);
        list.add(3);
        list.add(4);
        Set hs = new HashSet();
        1. 将list集合中的所有元素添加到Set集合中
        hs.addAll(list);
        2.Set集合作为List集合构造器的形参创建List集合实例
        System.out.println(new ArrayList(hs));

5.16.2 Set集合增删

remove过程:首先计算传入对象的哈希值,得到数组索引,当传入对象和索引位置的元素的哈希值相同并且调用equals()方法返回值为true时,移除相同元素。(跟add()比较流程相同)
当集合中一有元素的内容更改时,其哈希值不会更改。

set.remove(p1);:因为p1的属性name更改,所以其哈希值也随之更改,但是其在set集合中的位置没有改变。故调用remove(p1)方法时,查找的位置可能不是p1的当前位置,所以p1不会被移除。
set.add(new Person(1001,"CC"));:此对象的哈希值与集合中Person{1001,“CC”}的哈希值不同,所以两者不会出现哈希冲突,添加成功。
set.add(new Person(1001,"AA"));:此对象的哈希值与集合中Person{1001,“CC”}的哈希值相同,但是通过equals()比较内容不相同,所以添加成功。

HashSet set = new HashSet();
Person p1 = new Person(1001,"AA");
Person p2 = new Person(1002,"BB");
set.add(p1);
set.add(p2);
System.out.println(set);
p1.name = "CC";
set.remove(p1);
System.out.println(set);
set.add(new Person(1001,"CC"));
System.out.println(set);
set.add(new Person(1001,"AA"));
System.out.println(set);
[Person{name='BB', age=1002}, Person{name='AA', age=1001}]
[Person{name='BB', age=1002}, Person{name='CC', age=1001}]
[Person{name='BB', age=1002}, Person{name='CC', age=1001}, Person{name='CC', age=1001}]
[Person{name='BB', age=1002}, Person{name='CC', age=1001}, Person{name='CC', age=1001}, Person{name='AA', age=1001}]

6.Map集合

6.1 概述

6.1.1 Map的实现类的结构

|----Map:双列数据,存储key-value对的数据 —类似于高中的函数:y = f(x)
|--------HashMap:作为Map的主要实现类;线程不安全的,效率高;可以存储null的key和value
|------------LinkedHashMap:保证在遍历map元素时,可以按照添加的顺序实现遍历。
|------------原因:在原有的HashMap底层结构基础上,使用了一对双向链表来记录添加元素的顺序,迭代顺序与 Key-Value 对的插入顺序一致
|------------对于频繁的遍历操作,此类执行效率高于HashMap。
|--------TreeMap:保证按照添加的key-value对进行排序,实现排序遍历。此时考虑key的自然排序或定制排序,底层使用红黑树
|--------Hashtable:作为古老的实现类;线程安全的,效率低;不能存储null的key和value
|------------Properties:常用来处理配置文件。key和value都是String类型

HashMap的底层:数组+链表 (jdk7及之前);数组+链表+红黑树 (jdk 8)
在这里插入图片描述

6.1.2 细节

  1. Map 中的 key 和 value 都可以是任何引用类型的数据。
  2. key是唯一的,value可以重复,通过一个key只能确定唯一一个value值。

6.1.3 Map结构的理解

  1. key:不可重复且无序,所以使用Set存放。即同一个HashMap的key所对应的类,须重写hashCode()和equals()方法。
  2. value:可以重复且无序,使用Collection存放,value所对应的类应该重写equals()方法。
  3. Entry:key-value构成了一个Entry对象,无序且不可重复,使用Set存放。

6.2 HashMap

6.2.1 jdk1.7 底层源码分析

创建+添加:

  1. 通过空参构造器创建对象后,底层创建了长度是16的一维数组Entry[] table。
  2. 通过put(key,value)向集合中添加元素,==首先计算key的哈希值,==通过哈希值确定数组索引,如果数组索引处为空,添加成功;
  3. 如果数组索引处存在一个或多个entry对象(以链表形式存在),依次比较添加元素key与它们key的哈希值,如果都不同,添加成功(添加到链表尾部);
  4. 如果存在key的哈希值相同的entry对象,通过equals()依次与它们key的实体内容比较,如果所有的equals()都返回false,添加成功(添加到链表尾部);
  5. 如果任意一个equals()返回true,就用添加元素的value替换掉现有元素的value,并返回现有元素的value;

6.2.2 jdk1.8 底层源码分析

  1. 创建对象:通过空参构造器创建对象,此时并没有初始化数组Node<K,V>[] table长度,只是将填充因子loadFactor 设为DEFAULT_LOAD_FACTOR=0.75;
public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
  1. 首次添加元素:首先会创建一个长度为16,临界值(数组总长度*loadFactor)为12的Node数组,通过(n - 1) & hash(n:数组长度,hash:k的哈希值)计算出元素所在的数组位置,然后将元素放入数组中。
    之后添加元素:与jdk1.7大致相同。
    在这里插入图片描述
   public V put(K key, V value) {
   		hash(key):计算key的哈希值
        return putVal(hash(key), key, value, false, true);
    }
    
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                 boolean evict) {
      Node<K,V>[] tab; Node<K,V> p; int n, i;
      首次添加才能进入此if语句
      if ((tab = table) == null || (n = tab.length) == 0)
          n = (tab = resize()).length;
       如果添加元素所在的数组位置为null,则可以直接添加
      if ((p = tab[i = (n - 1) & hash]) == null)
          tab[i] = newNode(hash, key, value, null);
      else {
          Node<K,V> e; K k;
           如果 table 的索引位置的 key 的 hash 相同和新的 key 的 hash 值相同,
           并 满足(table 现有的结点的 key 和准备添加的 key 是同一个对象 || equals 返回真) 
           就认为不能加入新的 k-v
          if (p.hash == hash &&
              ((k = p.key) == key || (key != null && key.equals(k))))
              e = p;
          else if (p instanceof TreeNode)
          如果当前的 table 的已有的 Node 是红黑树,就按照红黑树的方式处理
              e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
          else {
              for (int binCount = 0; ; ++binCount) {
                  if ((e = p.next) == null) {
                      p.next = newNode(hash, key, value, null);
                      加入后,判断当前链表的个数,是否已经到 8 个,到 8 个,后 
                      就调用 treeifyBin 方法进行红黑树的转换
                      if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                          treeifyBin(tab, hash);
                      break;
                  }
                  if (e.hash == hash &&
                      ((k = e.key) == key || (key != null && key.equals(k))))
                      break;
                  p = e;
              }
          }
          if (e != null) { // existing mapping for key
              V oldValue = e.value;
              if (!onlyIfAbsent || oldValue == null)
                  e.value = value;
              afterNodeAccess(e);
              return oldValue;
          }
      }
      ++modCount;
      if (++size > threshold)
          resize();
      afterNodeInsertion(evict);
      return null;
  }

6.2.3 源码中的重要常量

DEFAULT_INITIAL_CAPACITY : HashMap的默认容量,16
DEFAULT_LOAD_FACTOR:HashMap的默认加载因子:0.75
threshold:扩容的临界值,数组总长度*加载因子:16 * 0.75 => 12
TREEIFY_THRESHOLD:Bucket中链表长度大于等于该默认值,转化为红黑树:8
MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量:64

6.2.4 扩容

添加元素时,当HashMap中元素的个数(1.7不包括要添加的元素,1.8包括)超过临界值(数组总长度*loadFactor)且要存放的位置非空,就会进行数组扩容。loadFactor(填充因子)的默认值时0.75,默认情况下,数组大小(DEFAULT_INITIAL_CAPACITY)为16,那么当HashMap中元素个数超过16*0.75=12(这个值就是代码中的threshold值,也叫做临界值)的时候,就把数组的大小扩展为2*16=32,临界值扩大为24(32*0.75),即扩大为原来的2倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数, 那么使用指定容量的构造器进行实例化能够有效地提高HashMap的性能。

6.2.5 为什么要提前扩容(为什么要有临界值)

因为HashMap中元素的索引是根据哈希值计算出来的,就可能会导致有的位置一直没有元素存放,而有的位置的元素会有很多个,降低查询效率,所以定义了临界值来提前扩容。

6.2.6 jdk1.8 链表 <==> 红黑树

添加元素成功之后,如果此索引位置上的链表长度大于等于TREEIFY_THRESHOLD=8但是数组长度小于MIN_TREEIFY_CAPACITY=64,那么HashMap会先扩容解决;如果数组长度大于等于64,那么此索引位置上的链表就会转为红黑树。后续如果由于删除或者其他原因调整了大小,当红黑树的节点小于或等于 6 个以后,又会恢复为链表形态。

为什么链表要变为红黑树:为了防止用户自己实现了不好的哈希算法时导致链表过长,从而导致查询效率低。
为什么数组有长度限制: 因为链表的长度不大的时候,红黑树不会带来明显的查询时间上的优势,反而会增加空间复杂度。所以通常情况下,并没有必要转为红黑树。
为什么TREEIFY_THRESHOLD是8:因为链表的长度不大的时候,红黑树不会带来明显的查询时间上的优势,反而会增加空间复杂度。所以通常情况下,并没有必要转为红黑树。如果 hashCode 分布良好,很少出现链表很长的情况。在理想情况下,链表长度符合泊松分布,长度为 8 概率小于千万分之一(仅为 0.00000006),所以通常情况下,并不会发生从链表向红黑树的转换。

    final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        //如果 table 为 null ,或者大小还没有到 64,暂时不树化,而是进行扩容. //否则才会真正的树化
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }

6.2.7 jdk1.8之前 HashMap 的缺点

  1. JDK 1.8 以前 HashMap 的实现是 数组+链表,即使哈希函数取得再好,也很难达到元素百分百均匀分布。
  2. 当 HashMap 中有大量的元素都存放到同一个桶中时,这个桶下有一条长长的链表,这个时候 HashMap 就相当于一个单链表,假如单链表有 n 个元素,遍历的时间复杂度就是 O(n),完全失去了它的优势。
  3. 针对这种情况,JDK 1.8 中引入了红黑树(查找时间复杂度为 O(logn))来优化查找时间。

6.2.8 jdk1.8 底层代码的改进

  1. 底层数组由Entry<K,V>[] table转为Node<K,V>[] table,两者没有较大的区别;
  2. HashMap map = new HashMap();默认情况下,先不创建长度为16的数组,当首次调用map.put()时,再创建长度为16的数组;
  3. 七上八下:jdk1.7时新增的元素作为链表头,jdk1.8时新增的元素作为链表尾;
  4. JDK 1.8 以前 HashMap 的实现是 数组+链表,jdk1.8之后是数组+链表+红黑树;

6.2.9 负载因子值的大小,对HashMap有什么影响

负载因子和当前Node数组长度的乘积称为临界值,根据临界值来进行数组的扩充。

  1. 负载因子的大小决定了HashMap的数据密度。
  2. 负载因子越大密度越大,发生碰撞的几率越高,数组中的链表越容易长,造成查询或插入时的比较次数增多,性能会下降。
  3. 负载因子越小,就越容易触发扩容,数据密度也越小,意味着发生碰撞的几率越小,数组中的链表也就越短,查询和插入时比较的次数也越小,性能会更高。但是会浪费一定的内容空间。而且经常扩容也会影响性能,建议初始化预设大一点的空间。
  4. 按照其他语言的参考及研究经验,会考虑将负载因子设置为0.7~0.75,此时平均检索长度接近于常数。

6.3 LinkedHashMap

6.3.1 细节

  1. 继承于HashMap,添加元素时使用双向链表来记录元素的顺序,所以遍历顺序与添加顺序一致。

6.3.2 实例

        Map m = new LinkedHashMap();
        m.put("指定",2);
        m.put("dsf",2);
        m.put("123",2);
        m.put(456,2);
        System.out.println(m);//{指定=2, dsf=2, 123=2, 456=2}

6.4 Map中的常用方法

6.4.1 添加/修改

  1. Object put(Object key,Object value):将指定key-value添加到(或修改)当前map对象中,返回值时被替换键值对的value,如果没有替换,则为null。
  2. void putAll(Map m):将集合m中的键值对添加到当前map集合中。

6.4.2 删除

  1. Object remove(Object key):移除指定key的key-value对,并返回value。
  2. void clear():清空当前map中的所有数据。

6.4.3 查找

  1. Object get(Object key):获取指定值对应的value
  2. boolean containsKey(Object key):是否包含指定的key
  3. boolean containsValue(Object value):是否包含指定的value
  4. int size():返回map中key-value对的个数
  5. boolean isEmpty():判断当前map是否为空
  6. boolean equals(Object obj):判断当前map和参数对象obj是否相等

6.4.4 元视图查询

  1. Set keySet():返回所有key构成的Set集合
  2. Collection values():返回所有value构成的Collection集合
  3. Set entrySet():返回所有key-value对构成的Set集合

6.4.5 Entry中的方法

  1. k getKey():获取entry中的键;
  2. k getValue():获取entry中的值;

6.5 遍历Map集合

6.5.1 方式1

获取entry集合,通过Entry中的getKey()和getValue()方法来遍历

        Set set1 = m.entrySet();
        for (Object o1 : set1) {
            System.out.print(((Map.Entry)o1).getKey());
            System.out.print(((Map.Entry)o1).getValue());
            System.out.println();
        }

6.5.2 方式2

获取keySet集合,通过get()方法来获取对应的value;

      Set set = m.keySet();
        for (Object o1 : set) {
            Object o2 = m.get(o1);
            System.out.println(o1 + "--->" + o2);
        }

6.6 TreeMap

6.6.1 细节

  1. TreeMap存放键值对时会对key进行排序,可以保证所有的 Key-Value 对处于有序状态。
  2. TreeMap底层使用红黑树结构存储数据。
  3. TreeMap中的所有Key必须是相同数据类型,且一定要实现比较器接口并重写其中的比较方法
  4. TreeMap判断两个key相等的标准:两个key通过compareTo()方法或者compare()方法返回0。

6.6.2 自然排序

key对象所在的类实现Comparable接口,重写compareTo()方法。

6.6.3 定制排序

实现Comparator接口,重写compare()方法,作为TreeMap的构造器参数传入;

      TreeMap t = new TreeMap(new Comparator<String>() {
           @Override
           public int compare(String o1, String o2) {
               return -o1.compareTo(o2);
           }
       });
        t.put("gsd",1);
        t.put("hsd",1);
        t.put("asd",1);
        t.put("csd",1);
        System.out.println(t);

6.7 Hashtable

  1. Hashtable实现原理和HashMap相同,功能相同。但是不同于HashMap,Hashtable是线程安全的
  2. 与HashMap不同,Hashtable 不允许使用 null 作为 key 和 value;

6.8 Properties

  1. Properties 类是 Hashtable 的子类,该对象用于处理属性文件
  2. 由于属性文件里的 key、value 都是字符串类型,所以 Properties 里的 key 和 value 都是字符串类型
  3. 存取数据时,建议使用setProperty(String key,String value)方法和getProperty(String key)方法
  4. 加载本地properties文件:pros.load(new FileInputStream("jdbc.properties"));
        Properties p = new Properties();
        FileInputStream fls = null;
        try {
            fls = new FileInputStream("test.properties");
            p.load(fls);
            Object name = p.getProperty("name");
            Object age = p.getProperty("age");
            System.out.println(name + "---" + age);
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                fls.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

7 开发中如何选择集合实现类

在这里插入图片描述

8 Collections

8.1 概述

Collections是操作List、Set、Map的工具类,其中都是静态方法,可以实现对集合的排序、查找、替换和线程安全化。

8.2 排序操作

8.2.1 静态方法

     * reverse(List):反转 List 中元素的顺序
     * shuffle(List):对 List 集合元素进行随机排序
     * sort(List):根据元素的自然顺序对指定 List 集合元素按升序排序
     * sort(ListComparator):根据指定的 Comparator 产生的顺序对 List 集合元素进行定制排序
     * swap(Listintint):将指定 list 集合中的 i 处元素和 j 处元素进行交换
     * Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
     * Object max(CollectionComparator):根据 Comparator 指定的顺序,返回给定集合中的最大元素
     * Object min(Collection)
     * Object min(CollectionComparator)
     * int frequency(CollectionObject):返回指定集合中指定元素的出现次数
     * void copy(List dest,List src):将src中的内容复制到dest中
     * boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换 List 对象的所有旧值

8.2.2 细节

  1. shuffle(List):对List集合随机排序,每次调用结果都不同。
  2. void copy(List dest,List src):将src中的内容复制到dest中。

list1中元素个数必须与list中相同,不然会抛异常Source does not fit in dest

List list1 = Arrays.asList(new Object[list.size()]);
Collections.copy(list1, list);
System.out.println(list1);

8.3 转为线程安全的集合

使用Collections.synchronizedXxx()方法可以将集合转为线程安全的集合。所以在实际开发中如果需要线程安全的集合,我们一般也不会选择Vector、Hashtable这种本来就线程安全的集合,而是使用Collections.synchronizedXxx()方法将ArrayList、HashMap转为线程安全的。
在这里插入图片描述