Java学习-集合篇

集合

什么是集合?有什么用?
数组其实就是一个集合。集合实际上就是一个容器。可以来容纳其他类型的数据。

集合为什么说在开发中使用的较多?
集合是一个容器,是一个载体,可以依次容纳多个对象。
在实际的开发中,假设连接数据库,数据库当中有10条记录,那么假设把这10条记录查询出来,在java程序中会将10条数据封装成10个java对象,然后将10个java对象放到某一个集合当中,将集合传到前端,然后遍历集合,将一个数据一个数据展现出来。

集合不能直接存储基本数据类型,另外集合也不能直接存储java对象,集合当中存储的都是java对象的内存地址。(或者说集合中存储的是引用。)
注意:集合在java中本身是一个容器,是一个对象。集合在任何时候存储的都是“引用”。

img

集合中存储的是对象的内存地址.png

在java中每一个不同的集合,底层会对应不同的数据结构。往不同的集合中存储元素,等于将数据放到了不同的数据结构当中。什么是数据结构?数据存储的结构就是数据结构。不同的数据结构,数据存储方式不同。
例如:数组、二叉树、链表、哈希表......这些都是常见的数据结构。
你往集合c1中放数据,可能是放到数组上了。
你往集合c2中放数据,可能是放到二叉树上了。
.......
你使用不同的集合等同于使用了不同的数据结构。

在我们这里,需要掌握的不是精通数据结构,java中已经将数据结构实现了,已经写好了这些常用的集合类,你只需要掌握怎么用?在什么情况下选择哪一种合适的集合去使用即可。
new ArrayList(); 创建一个集合,底层是数组。
new LinkedList(); 创建一个集合对象,底层是链表。
new TreeSet(); 创建一个集合对象,底层是二叉树。
......

集合在java JDK中java.util.*;包下。
所有的集合类和集合接口都在java.util包下。

在java中集合分为两大类:
一类是单个方式存储元素:
单个方式存储元素,这一类集合中超级父接口:java.util.Collection;
一类是以键值对儿的方式存储元素
以键值对的方式存储元素,这一类集合中超级父类接口:java.util.Map;

集合的继承结构

img

集合的继承结构图.png

img

List下的几个类.png

ArrayList集合底层采用了数组这种数据结构。
ArrayList集合是非线程安全的。
LinkedList集合采用了双向链表数据结构。
Vector集合底层采用了数组这种数据结构。
Vector集合是线程安全的。
Vector所有的方法都有synchronized关键字修饰,所以线程安全,但是效率较低,现在保证线程安全有别的方案,所以Vector使用的较少了。

img

Set下的类和接口.png

Set集合存储元素特点:无序不可重复,无序表示存进去是这个顺序,取出来就不一定是这个顺序了。
另外Set集合中元素没有下标,Set集合中的元素还不能重复。
实际上HashSet集合在new的时候,地城实际上new了一个HashMap集合。向HashSet集合中存储元素,实际上是存储到HashMap集合中了。
HashMap集合是一个哈希表数据结构。
TreeSet集合底层实际上是TreeMap。new TreeSet集合的时候,底层实际上new了一个TreeMap集合。
往TreeSet集合中放数据的时候,实际上是将数据放到TreeMap集合中了。
TreeMap集合底层采用了二叉树数据结构。
补充:SortedSet集合存储元素的特点:
由于继承了Set集合,所以它的特点也是无序不可重复,但是放在SortedSet集合中的元素可以自动排序。我们称为可排序集合。放到该集合中的元素是自动按照大小顺序排序的。

img

集合的继承结构图.png

img

Map集合继承结构图.png

总结:
ArrayList:底层是数组。
LinkedList:底层是双向链表。
Vector:底层是数组,线程安全的,效率较低,使用较少。
HashSet:底层是HashMap,放到HashSet集合中的元素等同于放到HashMap集合key部分了。
TreeSet:底层是TreeMap,放到TreeSet集合中的元素等同于放到TreeMap集合key部分了。
HashMap:底层是哈希表。
Hashtable:底层也是哈希表,只不过线程安全的,效率较低,使用较少。
Properties:是线程安全的,并且key和value只能存储字符串String。
TreeMap:底层是二叉树。TreeMap集合的key可以自动按照大小顺序排序。

List集合存储元素的特点:
有序可重复。
有序:存进去的顺序和取出的顺序相同,每一个元素都有下标。
可重复:存进去1,还可以再存储一个1.

Set(Map)集合存储元素的特点:
无序不可重复。
无序:存进去的顺序和取出的顺序不一定相同。另外Set集合中元素没有下标。
不可重复:存进去1,就不能再存储1了。

SortedSet(SortedMap)集合存储元素的特点:
首先是无序不可重复的,但是SortedSet集合中的元素是可排序的。
无序:存进去的顺序和取出的顺序不一定相同。另外Set集合中元素没有下标。
不可重复:存进去1,就不能再存储1了。
可排序:可以按照大小顺序排列。

Map集合的key,就是一个Set集合。
在Set集合中放数据,实际上放到了Map集合的key部分。

Collection接口

Collection中能存放什么元素?
没有使用“泛型”之前,Collection中可以存储Object的所有子类型。
使用了“泛型”之后,Collection中只能存储某个具体的类型。
集合后期会学习“泛型”语法。目前先不用管。Collection中什么都能存,只要是Object的子类型就行。(集合中不能直接存储基本数据类型,也不能存java对象,只是存储java对象的内存地址。)

Collection中的常用方法

关于java.util.Collection接口中常用的方法。

boolean add(E e) 向集合中添加元素 (E是泛型,目前看成Object就行)

//创建一个集合对象
//多态
Collection c = new ArrayList();
c.add(1200);//自动装箱(java5新特性)
c.add(3.14);//自动装箱
c.add(new Object());
c.add(true);//自动装箱

int size() 获取集合中元素的个数

//获取集合中元素的个数
System.out.println("集合中元素个数是:" + c.size());//4

void clear() 清空集合

//清空集合
c.clear();
System.out.println("集合中元素个数是:" + c.size());//0

boolean contains(Object o) 判断当前集合中是否包含元素o,包含返回true,不包含返回false

//再向集合中添加元素
c.add("hello");//"hello"对象的内存地址放到了集合中
c.add("world");
c.add("浩克");
c.add("绿巨人");
c.add(1);
//判断集合中是否包含“绿巨人”
bollean flag = c.contains("绿巨人");
bollean flag1 = c.contains("绿巨人2");
System.out.println(flag);//true
System.out.println(flag1);//false
System.out.println(c.contains(1));//true
//获取集合中元素的个数
System.out.println("集合中元素个数是:" + c.size());//5

bollean remove(Object o) 删除集合中某个元素

c.remove("1");
//获取集合中元素的个数
System.out.println("集合中元素个数是:" + c.size());//4

boolean isEmpty() 判断该集合中元素的个数是否为0

//判断集合是否为空(集合中是否存在元素)
System.out.println(c.isEmpty());//false
c.clear();//清空
System.out.println(c.isEmpty());//true

Object[] toArray() 调用这个方法可以把集合转换成数组。

c.add("abc");
c.add("def");
c.add(100);
c.add("helloworld");

//转换成数组(了解,使用不多)
Object[] obj = c.toArray();
for(int i = 0; i < obj.length ; i++){
  //遍历数组
  Object o = obj[i];
  System.out.println(o);
}

关于集合遍历/迭代

以下学习的遍历方式/迭代凡是,事所有Collection通用的一种方式。
在Map集合中不能用。在所有的Collection以及子类中使用。

//创建集合对象
Collection c = new ArrayList();//后面的集合无所谓,主要是看前面的Collection接口,怎么遍历/迭代。
//添加元素
c.add("abc");
c.add("def");
c.add(100);
c.add(new Object());
//对集合Collection进行遍历/迭代
//第一步:获取集合对象的迭代器对象Iterator
Iterator it = c.iterator();
//第二步:通过以上获取的迭代器对象开始迭代/遍历集合。
/*
    以下两个方法是迭代器对象Iterator中的方法:
        boolean hasNext()  如果仍有元素可以迭代,则返回true
        Object next() 返回迭代的下一个元素
*/
while(it.hasNext){
  //不管你当初存进去什么,取出来统一都是Object。
  Object obj = it.next();
  System.out.println(obj);
}

img

迭代器执行原理.png

img

迭代原理.png

深入Colllection集合的contains方法:
boolean contains(Object o)
判断集合中是否包含某个对象o
如果包含返回true,如果不包含返回false

contains方法是用来判断集合中是否包含某个元素的方法,那么它在底层是怎么判断集合中是否包含某个元素的呢?
调用了equals方法进行比对。
equals方法返回true,就表示包含这个元素。

存放在集合中的类型,一定要重写equals方法。

Collection接口中的remove方法和contains方法底层都会调用equals。

关于集合元素的remove:
重点:当集合的结构发生改变时,迭代器必须重新获取,如果还是用以前老的迭代器,会出现异常:java.ConcurrentModificationException

重点:在迭代集合元素的过程中,不能调用集合对象的remove方法,删除元素:c.remove(o);迭代过程中不能这样。

出异常的根本原因是:集合中元素删除了,但是没有更新迭代器(迭代器不知道集合变化了。)

直接通过集合去删除元素,没有通知迭代器,导致迭代器的快照和原集合状态不同。
迭代器去删除时,会自动更新迭代器,并且更新集合(删除集合中的元素)。

在迭代元素的过程当中,一定要使用迭代器Iterator的remove方法,删除元素,不要使用集合自带的remove方法删除元素。

List接口的常用方法

LIst集合存储元素特点:有序可重复
有序:List集合中的元素有下标。从0开始,以1递增
可重复:存储一个1,还可以再存储1。

List既然是Collection接口的子接口,那么肯定List接口有自己“特色”的方法:
以下只列出List接口特有的常用的方法:
(目前先把E看成Object)
void add(int index,E element)
E get(int index)
int indexOf(Object o)
int lastIndexOf(Object o)
E remove(int index)
E set(int index,E element)

这些方法不需要死记硬背,建议自己编写代码测试一下,理解一下,以后开发的时候,还是要翻阅帮助文档。

//创建List类型的集合。
List myLIst = new ArrayList();

//添加元素
myList.add("A");
myList.add("B");
myList.add("C");
myList.add("C");
myList.add("D");
//在列表的指定位置插入指定元素(第一个参数是下标)
//这个方法使用不多,因为对于ArrayList集合来说效率比较低。
myList.add(1,"KING")

//迭代
Iterator it = myList.iterrator();
while(it.hasNext()){
  Object elt = it.next();
  System.out.println(elt);
}

//根据下标获取元素
Object firstObj = myList.get(0);
System.out.println(firstObj);

//因为有下标所以List集合有自己比较特殊的遍历方式
//通过下标遍历。【List集合特有的方式,Set没有】
for(int i = 0; i < myList.size(); i++){
  Object obj = myList.get(i);
  System.out.println(obj);
}

//获取指定对象第一次出现处的索引。
System.out.println(myList.indexOf("KING"));//1

//获取指定对象最后一次出现处的索引
System.out.println(myList.lastIndexOf("C"));//4

//删除指定下标位置的元素
myList.remove(0);//删除下标为0的元素
System.out,println(myList.size());//5

System.out.println("===================");

//修改指定位置的元素
myList.set(2,"Soft");

//遍历集合
for(int i = 0; i < myList.size(); i++){
  Object obj = myList.get(i);
  System.out.println(obj);
}
 

计算机英语:
增删改查这几个单词要知道
增:add、save、new
删:delete、drop、remove
改:update、set、modify
查:find、get、query、select

ArrayList集合

ArrayList集合初始化容量是10。(底层先创建了一个长度为0的数组,当添加第一个元素的时候,初始化容量10)

ArrayList集合底层是Object类型的数组Object[]。

构造方法:
new ArrayList();
new ArrayList(20);

//默认初始化容量10
List myList1 = new ArrayList();

//指定初始化容量20
List myList2 = new ArrayList(20);

ArrayList集合的扩容:增长到原容量的1.5倍。

ArrayList底层是数组,怎么优化?
尽可能少的扩容。因为数组扩容效率比较低,建议在使用ArrayList集合的时候预估计元素的个数,给定一个初始化容量。这是ArrayList集合比较重要的优化策略。

数组的优点:检索效率比较高(每个元素占用的空间大小相同,内存地址是连续的,知道首元素内存地址,然后知道下标,通过数学表达式计算出元素的内存地址,所以检索效率最高。)
数组的缺点:随机增删元素效率比较低。另外,数组无法存储大数据量。(很难找到一块非常巨大的连续的内存空间。)
但是需要注意的是:向数组末尾添加元素,效率还是很高的。

面试官经常问的一个问题:
这么多的集合中你用哪个集合最多?
答:ArrayList集合。
因为往数组末尾添加元素,效率不受影响。另外,我们检索/查找某个元素的操作比较多。

ArrayList集合还有另一个构造方法

//创建一个HashSet集合
Collection c = new HashSet();
//添加元素到Set集合
c.add(100);
c.add(200);
c.add(900);
c.add(50);

//通过这个构造方法就可以将HashSet集合转换成List集合。
List myList = new ArrayList(c);
for(int i = 0; i < myLisst.size(); i++){
  System.out.println(myList.get(i));
}

位运算

右移 >>
左移 <<
右移n位就是除以2的n次方
左移n位就是乘以2的n次方

LinkedList集合

单向链表数据结构
对于链表数据结构来说:基本的单元是节点Node。
对于单向链表来说:任何一个节点Node中都有两个属性:
第一:存储的数据。第二:下一节点的内存地址。

img

单向链表.png

链表优点:随机增删元素效率较高。(因为增删元素不涉及到大量元素位移)
由于链表上的元素在空间存储上内存地址不连续。所以随即增删元素的时候不会有大量元素位移,因此随即增删效率较高。在以后的开发中,如果遇到随即增删集合中元素的业务比较多时,建议使用LinkedList。

链表缺点:查询效率较低,每一次查找某个元素的时候都需要从头节点开始往下遍历。
不能通过数学表达式计算被查找元素的内存地址,每一次查找都是从头节点开始遍历,直到找到为止。所以LinkedList集合检索/查找的效率较低。

ArrayList集合之所以检索效率比较高,不是单纯因为下标的原因。是因为底层数组发挥的作用。
LinkedList集合照样有下标,但是检索/查找某个元素的时候效率比较低,因为智能从头节点开始一个一个遍历。

ArrayList集合是非线程安全的。(不是线程安全的集合。)

img

双向链表.png

LinkedList集合没有初始化容量。
最初这个链表中没有任何元素。first和last引用都是null。
不管是LinkedList还是ArrayList,以后写代码时不需要关心具体是哪个集合。
因为我们要面向接口编程,调用的方法都是接口中的方法。

LinkedList集合底层数据结构是双向链表。
对于链表数据结构来说,随即增删效率较高。检索效率较低。
链表中的元素在空间存储上,内存地址不连续。

Vector集合

Vector底层也是一个数组。
默认初始化容量是10。
扩容之后是原容量的2倍。
Vector中所有的方法都是线程同步的,都带有synchronized关键字,是线程安全的。效率比较低,使用较少了。

怎么将一个线程不安全的ArrayList集合转换成线程安全的呢?
使用集合工具类:java.util.Collections;

java.util.Collection是集合接口。
java.util.Collections是集合工具类。

List myList = new ArrayList();//非线程安全的。

//变成线程安全的
Collections.synchronizedList(myList);

//此时myList集合就是线程安全的
myList.add("111");
myList.add("222");
myList.add("333");

泛型

JDK5之后的新特性:泛型
泛型这种语法机制,直在程序编译阶段起作用,只是给编译器参考的。(运行阶段泛型没用!)

使用泛型好处是什么?
第一:集合中存储的元素类型统一了。
第二:从集合中取出的元素类型是泛型指定的类型,不需要进行大量的“向下转型”。

泛型的缺点是什么?
导致集合中存储的元素缺乏多样性!
大多数业务中,集合中元素的类型还是统一的。所以这种泛型特性被大家所认可。

//创建集合对象
//使用泛型List<Animal>之后,表示List集合中只允许存储Animal类型的数据。
//用泛型来指定集合中存储的数据类型。
List<Animal> myList = new ArrayList<Animal>();

//准备对象
Cat c = new Cat();
Bird b = new Bird();
myList.add(c);
myList.add(b);

//获取迭代器
//这个表示迭代器迭代的是Animal类型。
Iterator<Animal> it = myList.iterator();
while(it.hasNext()){
    //使用泛型之后,每一次迭代返回的数据都是Animal类型。
    Animal a = it.next();
    //这里不需要进行强制类型转换了。直接调用。
    a.move();
    //调用子类特有方法还是需要向下转换的!
    Animal a = it.next();
    if(a instanceof Cat){
        Cat x = (Cat)a;
        x.catchMouse();
    }

    if(a instanceof Bird){
        Bird y = (Bird)a;
        y.fiy();
    }
}


class Animal{
    public void move{
        System.out.println("动物在移动");
    }
}

class Cat extends Animal{
    public void catchMouse(){
        System.out.println("猫抓老鼠");
    }
}

class Bird extends Animal{
    public void fly(){
        System.out.println("鸟儿在飞翔");
    }
}

JDK8之后,引入了:自动类型推断机制(又称为钻石表达式)

//ArrayList<这里的类型会自动推断>,前提是JDK8之后才允许
//自动类型推断,钻石表达式
List<Animal> myList = new ArrayList<>();

自定义泛型可以么?可以
自定义泛型的时候,<>尖括号中的是一个标识符,随便写。
java源代码中经常出现的是:
E是Element单词首字母。
T是Type单词首字母。

增强for循环(foreach)

语法:
for(元素类型 变量名 : 数组或集合){
System.out.println(变量名);
}

foreach有一个缺点:没有下标。在需要使用

HashSet集合

存储时顺序和取出时顺序不同。
不可重复。
放到HashSet集合中的元素实际上是放到HashMap集合的key部分了。

TreeSet集合

TreeSet集合存储元素特点:
无序不可重复的,但是存储的元素可以自动按照大小顺序排序!
称为:可排序集合。
这里的无序指的是存进去的顺序和取出来的顺序不同。并且没有下标。

Map集合

Map和Collection没有继承关系。
Map集合以key和value的方式存储数据:键值对
key和value都是引用数据类型。
key和value都是存储对象的内存地址。
key起到主导地位,value是key的一个附属品。

Map集合的常用方法

V put(K key , V value) 向Map集合中添加键值对
V get(Object key) 通过key获取value
void clear() 清空Map集合
boolean containsKey(Object key) 判断Map中是否包含某个key
boolean containsValue(Object value) 判断Map中是否包含某个value
boolean isEmpty() 判断Map集合中元素个数是否为0
Set keySet() 获取Map集合中元素个数是否为0
V remove(Object key) 通过key删除键值对
int size() 获取Map集合中键值对的个数
Collection values() 获取Map集合中所有的value,返回一个Collection
Set<Map.Entry<K,V>> entrySet() 将Map集合转换成Set集合

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

public class Text {
    public static void main(String[] args) {

        //创建map集合对象
        Map<Integer,String> map = new HashMap<>();

        //向map集合中添加键值对
        map.put(1,"zhangsan");//这里1进行了自动装箱
        map.put(2,"lisi");
        map.put(3,"wangwu");
        map.put(4,"zhaoliu");

        //通过key获取value
        String value = map.get(2);
        System.out.println(value);

        //获取键值对的数量
        System.out.println("键值对的数量:" + map.size());//4

        //通过key删除key-value
        map.remove(2);
        System.out.println("键值对的数量:" + map.size());//3

        //判断是否包含某个key
        //contains方法底层调用的都是equals进行比对的,所以自定义类型需要重写equals方法。
        System.out.println(map.containsKey(4));//true
        //System.out.println(map.containsKey(new Integer(4)));//true

        //判断是否包含某个value
        System.out.println(map.containsValue("wangwu"));//true
        //System.out.println(map.containsValue(new String("wangwu")));//true

        //获取所有的value
        Collection<String> values = map.values();
        //foreach
        for (String s : values) {
            System.out.println(s);
        }

        //清空map集合
        map.clear();
        System.out.println("键值对的数量:" + map.size());//0

        //判断是否为空
        System.out.println(map.isEmpty());//true
    }
}

重要的两个方法。

import java.util.*;

/*
    Map集合的遍历
*/
public class Text {
    public static void main(String[] args) {
        //第一种方式:获取所有的key,通过遍历key,来遍历value
        Map<Integer,String> map = new HashMap<>();
        map.put(1,"zhangsan");
        map.put(2,"lisi");
        map.put(3,"wangwu");
        map.put(4,"zhaoliu");
        //遍历Map集合
        //获取所有的key,所有的key是一个Set集合
        Set<Integer> keys =  map.keySet();
        //遍历key,通过key获取value
        //迭代器可以
        /*Iterator<Integer> it = keys.iterator();
        while (it.hasNext()){
            //取出其中一个key
            Integer key = it.next();
            //通过key获取value
            String value = map.get(key);
            System.out.println(key + "=" + value);
        }*/
        //foreach也可以
        for (Integer key: keys) {
            System.out.println(key + "=" + map.get(key));
        }

        //第二种方式:Set<Map.Entry<K,V>> entrySet()
        //以上这个方法是把Map集合直接全部转换成Set集合
        //Set集合中元素的类型是:Map.Entry
        Set<Map.Entry<Integer,String>> set = map.entrySet();
        //遍历Set集合,每一次取出一个Node
        //迭代器
       /* Iterator<Map.Entry<Integer,String>> it2 = set.iterator();
        while (it2.hasNext()){
            Map.Entry<Integer,String> node = it2.next();
            Integer key = node.getKey();
            String value = node.getValue();
            System.out.println(key + "=" + value);
        }*/
        
        //foreach
        //这种方式效率比较高,因为获取key和value都是直接从node对象中获取的属性值。
        //这种方式比较适合于大数据量
        for (Map.Entry<Integer,String> node: set) {
            System.out.println(node.getKey() + "---->" + node.getValue());
        }
    }
}

哈希表数据结构

HashMap集合底层是哈希表/散列表的数据结构。

哈希表是一个怎样的数据结构呢?
哈希表是一个数组和单向链表的结合体。
数组:在查询方面效率很高,随即增删方面效率很低。
单向链表:在随即增删方面效率很高,在查询方面效率很低。
哈希表将以上的两种数据结构融合在一起,充分发挥它们各自的优点。

HashMap集合底层的源代码:

public class HashMap{
    //HashMap底层实际上就是一个数组。(一维数组)
    transient Node<K,V>[] table

    //静态的内部类HashMap.Node
    static class Node<K,V> implements Map.Entry<K,V>{
        final int hash;//哈希值(哈希值是key的hashCode()方法的执行结果。hash值通过哈希函数/算法,可以转换存储成数组的下标。)
        final K key;//存储到Map集合中的那个key
        V value;//存储到Map集合中的那个value
        Node<K,V> next;//下一个节点的内存地址
    }
}

哈希表/散列表:一维数组,这个数组中每一个元素是一个单向链表。(数组和链表的结合体。)

最主要掌握的是:
map.put(k,v)
v = map.get(k)
以上这两个方法的实现原理,是必须掌握的。

map.put(k,v)实现原理:
第一步:先将k,v封装到Node对象当中。
第二步:底层会调用k的hashCode()方法得出hash值,通过通过哈希函数/哈希算法,将hash值转换成数组下标,下标位置上如果没有任何元素,就把Node添加到这个位置上了。如果说下标对应的位置上有链表,此时会拿着k和链表上每一个节点中的k进行equals,如果所有的equals方法返回都是false,那么这个新节点将会被添加到链表的末尾。如果其中有一个equals返回了true,那么这个节点的value将会被覆盖。

v = map.get(k)实现原理:
先调用k的hashCode()方法得出哈希值,通过哈希算法转换成数组下标,通过数组下标快速定位到某个位置上,如果这个位置上什么都没有,返回null,如果这个位置上有单向链表,那么拿着参数k和单向链表上的每个节点中的k进行equals,如果所有equals方法返回false,那么get方法返回null,只要其中有一个节点的k和参数k equals的时候返回true,那么此时这个节点的value就是我们要找的value,get方法最终返回这个要找的value。

为什么哈希表的随即增删以及查询效率都很高?
增删是在链表上完成。
查询也不需要都扫描,只需要部分扫描。

HashMap集合的key,会先后调用两个方法,一个方法是hashCode(),一个方法是equals(),那么这两个方法都需要重写。

HashMap集合的key部分特点:
无序,不可重复。
为什么无序?因为不一定挂在哪个单向链表上。
不可重复是怎么保证的?equals方法来保证HashMap集合的key不可重复。
如果key重复了,value会覆盖。

放在HashMap集合key部分的元素其实就是放到HashSet集合中了。所以HashSet集合中的元素也需要同时重写hashCode()+equalis()方法。

注意:同一个单项链表上,所有节点的hash相同,因为他们的数组下标是一样的,但同一个链表上k和k的equals方法肯定返回的是false,都不相等。

哈希表HashMap使用不当时无法发挥性能!
假设将所有的hashCode()方法返回值固定为某个值,那么会导致底层哈希表变成了纯单向链表。这种情况我们称为:散列分布不均匀。

什么是散列分布均匀?
假设有100个元素,10个单向链表,那么每个单向链表上有10个节点,这是最好的,是散列分布均匀的。

假设将所有的hashCode()方法返回值都设定为不一样的值,可以吗,有什么问题?
不行,这样的话导致底层哈希表就成为一维数组了,没有链表的概念了。也是散列分布不均匀。
散列分布均匀需要你重写hashCode()方法时有一定的技巧。

重点:放在HashMap集合key部分的元素,以及放在HashSet集合中的元素,需要同时重写hashCode和equals方法。

HashMap集合的默认初始化容量是16,默认加载因子是0.75。
这个默认加载因子是当HashMap集合底层数组的容量达到75%的时候,数组开始扩容。
重点,记住:HashMap集合初始化容量必须是2的倍数,这也是官方推荐的,这是因为达到散列分布均匀,为了提高HashMap集合的存取效率,所必须的。

向Map集合中存,以及从Map集合中取,都是先调用key的hashCode方法,然后再调用equals方法!
equals方法有可能调用,也有可能不调用。
拿put(k,v)举例,什么时候equals不会调用?
k.hashCode()方法返回哈希值,哈希值经过哈希算法转换成数组下标。数组下标位置上如果是null,equals不需要执行。
拿get(k)举例,什么时候equals不会调用?
k.hashCode()方法返回哈希值,哈希值经过哈希算法转换成数组下标。数组下标位置上如果是null,equals不需要执行。

注意:如果一个类的equals方法重写了,那么hashCode()方法必须重写。并且equals方法返回如果是true,hashCode()方法返回的值必须一样。
equals方法返回true表示两个对象相同,再同一个单向链表上比较。那么对于同一个单向链表上的节点来说,他们的哈希值都是相同的。所以hashCode()方法的返回值也应该相同。

hashCode()方法和equals()方法也不用研究了,直接使用IDEA工具生成,但是这两个方法需要同时生成。

终极结论:放在Map集合key部分的,以及放在HashSet集合中的元素,需要同时重写hashCode方法和equals方法。

在JDK8之后,如果哈希表单向链表中元素超过8个,单向链表这种数据结构会变成红黑树数据结构。当红黑树上的节点数量小于6时,会重新把红黑树变成单向链表数据结构。
这种方式也是为了提高检索效率,二叉树的检索会再次缩小扫描范围。提高效率。初始化容量16,默认加载因子.75。

对于哈希表数据结构来说:
如果o1和o2的hash值相同,一定是放到同一个单向链表上。
当然如果o1和o2的hash值不同,但由于哈希算法执行结束之后转换的数组下标可能相同,此时会发生“哈希碰撞”。

扩容之后的容量时原容量的2倍。

Hashtable

HashMap集合key部分允许null吗?
允许。但是要注意:HashMap集合的key null值只能有一个。

Hashtable的key和value都是不能为null的。
HashMap集合的key和value都是可以为null的。

Hashtable方法都带有synchronized:线程安全的。
线程安全有其他的方案,这个Hashtable对线程的处理导致效率较低,使用较少了。

Hashtable和HashMap一样,底层都是哈希表数据结构。

Hashtable的初始化容量是11,默认加载因子是:0.75f

Hashtable的扩容是:原容量 * 2 + 1

属性类Properties类

目前只需要掌握Properties属性类对象的相关方法即可。
Properties是一个Map集合,继承Hashtable,Properties的key和value都是String类型。
Properties被称为属性类对象。
Properties是线程安全的。

//创建一个Properties对象
Properties pro = new Properties();

//需要掌握Properties的两个方法,一个存,一个取。
pro.setProperty("url","jdbc:mysql://localhost:3306/powernode");
pro.setProperty("driver","com.mysql.jdbc.Driver");
pro.setProperty("username","root“);
pro.setProperty("password","123");

//通过key获取value
String url = pro.getProperty("url");
String driver = pro.getProperty("driver");
String username = pro.getProperty("username");
String password = pro.getProperty("password");

System.out.println(url);
System.out.println(driver);
System.out.println(username);
System.out.println(password);

TreeSet

TreeSet集合底层实际上是一个TreeMap。
TreeMap集合底层是一个二叉树。
放到TreeSet集合中的元素,等同于放到TreeMap集合key部分了。
TreeSet集合中的元素:无序不可重复,但是可以按照元素的大小顺序自动排序。称为:可排序集合。

//创建对象
TreeSet<String> ts = new TreeSet<>();
//添加元素
ts.add("zhangsan");
ts.add("lisi");
ts.add("wangwu");
ts.add("zhangsi");
ts.add("wangliu");
//遍历
for(String s : ts ){
    //按照字典顺序,升序!
    System.out.println(s);
}

TreeSet<Integer> ts2 = new TreeSet<>();
ts2.add(100);
ts2.add(200);
ts2.add(900);
ts2.add(800);
ts2.add(600);
ts2.add(10);
for(Integer elt : ts2){
    //升序
    System.out.println(elt);
}

对于自定义类型来说,TreeSet无法排序,因为你没有指定比较规则。没有实现java.long.Comparable接口。
指定规则就需要把Comparable接口的方法实现,编写出比较的逻辑,或者是比较的规则。

compareTo方法的返回值很重要:
返回0表示相同,value会覆盖。
返回>0,会继续在右子树上找。
返回<0,会继续在左子树上找。

自平衡二叉树数据结构

TreeSet集合/TreeMap集合是自平衡二叉树,遵循左小右大原则存放。

遍历二叉树的时候有三种方式:
前序遍历:根左右
中序遍历:左根右
后序遍历:左右根
注意:前中后说的是“根”的位置,根在前面是前序,根在中间是中序,根在后面是后序。

TreeSet集合/TreeMap集合采用的是:中序遍历方式。
Iterator迭代器采用的是中序遍历方式。
左根右。

img

自平衡二叉树.png

比较器实现java.util.Comparator接口。(Comparator是java.lang包下的。Comparator是java.util包下的。)

最终结论:放到TreeSet或者TreeMap集合key部分的元素要想做到排序,包括两种方式:
第一种:放在集合中的元素实现java.lang.Comparable接口。
第二种:在构造TreeSet或者TreeMap集合的时候给它传一个比较器对象。

Comparable和Comparator怎么选择?
当比较规则不会发生改变的时候,或者说当比较规则只有1个的时候,建议实现Comparable接口。
如果比较规则有多个,并且需要多个比较规则之间频繁切换,建议使用Comparator接口。

Comparator接口的设计符合OCP原则。

Collections工具类

java.util.Collection 集合接口
java.util.Collections 集合工具类,方便集合的操作。

//ArrayList集合不是线程安全的
List<String> list = new ArrayList<>();

//变成线程安全的
Collections.synchronizedList(list);

//排序
list.add("abf");
list.add("abx");
list.add("abc");
list.add("abe");

Collections.sort(list);
for(String s : list){
    System.out.println(s);
}

//注意:对List集合中元素排序,需要保证List集合中的元素实现了:Comparable接口。

//对Set集合怎么排序呢?
Set<String> set = new HashSet<>();
set.add("king");
set.add("kingsoft");
set.add("king2");
set.add("king1");
//将Set集合转换成List集合
List<String> myList = new ArrayList<>(set);
Collections.sort(myList);
for(String s : myList){
    System.out.println(s);
}
//这种方式也可以排序
//Collections.sort(List集合,比较器对象);

最后编辑于 :2020.11.15 19:33:57

posted @ 2023-06-23 09:51  哩个啷个波  阅读(5)  评论(0编辑  收藏  举报