Java学习-集合篇
集合
什么是集合?有什么用?
数组其实就是一个集合。集合实际上就是一个容器。可以来容纳其他类型的数据。
集合为什么说在开发中使用的较多?
集合是一个容器,是一个载体,可以依次容纳多个对象。
在实际的开发中,假设连接数据库,数据库当中有10条记录,那么假设把这10条记录查询出来,在java程序中会将10条数据封装成10个java对象,然后将10个java对象放到某一个集合当中,将集合传到前端,然后遍历集合,将一个数据一个数据展现出来。
集合不能直接存储基本数据类型,另外集合也不能直接存储java对象,集合当中存储的都是java对象的内存地址。(或者说集合中存储的是引用。)
注意:集合在java中本身是一个容器,是一个对象。集合在任何时候存储的都是“引用”。
集合中存储的是对象的内存地址.png
在java中每一个不同的集合,底层会对应不同的数据结构。往不同的集合中存储元素,等于将数据放到了不同的数据结构当中。什么是数据结构?数据存储的结构就是数据结构。不同的数据结构,数据存储方式不同。
例如:数组、二叉树、链表、哈希表......这些都是常见的数据结构。
你往集合c1中放数据,可能是放到数组上了。
你往集合c2中放数据,可能是放到二叉树上了。
.......
你使用不同的集合等同于使用了不同的数据结构。
在我们这里,需要掌握的不是精通数据结构,java中已经将数据结构实现了,已经写好了这些常用的集合类,你只需要掌握怎么用?在什么情况下选择哪一种合适的集合去使用即可。
new ArrayList(); 创建一个集合,底层是数组。
new LinkedList(); 创建一个集合对象,底层是链表。
new TreeSet(); 创建一个集合对象,底层是二叉树。
......
集合在java JDK中java.util.*;包下。
所有的集合类和集合接口都在java.util包下。
在java中集合分为两大类:
一类是单个方式存储元素:
单个方式存储元素,这一类集合中超级父接口:java.util.Collection;
一类是以键值对儿的方式存储元素
以键值对的方式存储元素,这一类集合中超级父类接口:java.util.Map;
集合的继承结构
集合的继承结构图.png
List下的几个类.png
ArrayList集合底层采用了数组这种数据结构。
ArrayList集合是非线程安全的。
LinkedList集合采用了双向链表数据结构。
Vector集合底层采用了数组这种数据结构。
Vector集合是线程安全的。
Vector所有的方法都有synchronized关键字修饰,所以线程安全,但是效率较低,现在保证线程安全有别的方案,所以Vector使用的较少了。
Set下的类和接口.png
Set集合存储元素特点:无序不可重复,无序表示存进去是这个顺序,取出来就不一定是这个顺序了。
另外Set集合中元素没有下标,Set集合中的元素还不能重复。
实际上HashSet集合在new的时候,地城实际上new了一个HashMap集合。向HashSet集合中存储元素,实际上是存储到HashMap集合中了。
HashMap集合是一个哈希表数据结构。
TreeSet集合底层实际上是TreeMap。new TreeSet集合的时候,底层实际上new了一个TreeMap集合。
往TreeSet集合中放数据的时候,实际上是将数据放到TreeMap集合中了。
TreeMap集合底层采用了二叉树数据结构。
补充:SortedSet集合存储元素的特点:
由于继承了Set集合,所以它的特点也是无序不可重复,但是放在SortedSet集合中的元素可以自动排序。我们称为可排序集合。放到该集合中的元素是自动按照大小顺序排序的。
集合的继承结构图.png
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);
}
迭代器执行原理.png
迭代原理.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中都有两个属性:
第一:存储的数据。第二:下一节点的内存地址。
单向链表.png
链表优点:随机增删元素效率较高。(因为增删元素不涉及到大量元素位移)
由于链表上的元素在空间存储上内存地址不连续。所以随即增删元素的时候不会有大量元素位移,因此随即增删效率较高。在以后的开发中,如果遇到随即增删集合中元素的业务比较多时,建议使用LinkedList。
链表缺点:查询效率较低,每一次查找某个元素的时候都需要从头节点开始往下遍历。
不能通过数学表达式计算被查找元素的内存地址,每一次查找都是从头节点开始遍历,直到找到为止。所以LinkedList集合检索/查找的效率较低。
ArrayList集合之所以检索效率比较高,不是单纯因为下标的原因。是因为底层数组发挥的作用。
LinkedList集合照样有下标,但是检索/查找某个元素的时候效率比较低,因为智能从头节点开始一个一个遍历。
ArrayList集合是非线程安全的。(不是线程安全的集合。)
双向链表.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
V remove(Object key) 通过key删除键值对
int size() 获取Map集合中键值对的个数
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迭代器采用的是中序遍历方式。
左根右。
自平衡二叉树.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