数组VS集合
数组(长度开始时必须指定,而且一旦指定,不能更改)(增加/删除元素比较麻烦)
1)长度开始时必须指定,而且一旦指定,不能更改
2)保存的必须为同一类型的元素
3)使用数组进行增加/删除元素比较麻烦
集合(动态保存任意多个对象)(增加/删除元素比较方便)
1)可以动态保存任意多个对象
,使用比较方便!
2)提供了一系列方便的操作对象的方法:add、remove、set、get等
3)使用集合添加/删除新元素很简洁
集合的框架体系
List 接口、Set接口 ( 他们的实现子类都是单列集合
)
Map 接口(实现子类是双列集合,存放的 K-V
)
线程安全不安全介绍
线程不安全
ArrayList, LinkedList,
HashSet, LinkedHashSet,TreeSet
HashMap , LinkedHashMap,TreeMap
线程安全
内置
Vector
Hashtable
并发包
CopyOnWriteArrayList
CopyOnWriteArraySet
ConcurrentHashMap
通过包装
Collections.synchronizedList
Collections.synchronizedSet
Collections.synchronizedMap
Collection 接口和常用方法
Collection 接口实现类的特点
Collection 接口常用方法,以实现子类 ArrayList 来演示
增 add,addAll
删 remove(元素|或下标)
查 contains
大小容量 size,isEmpty
清空 clear
import java.util.ArrayList;
import java.util.List;
/**
* @创建人 wdl
* @创建时间 2024/9/6
* @描述
*/
public class CollectionMethod {
@SuppressWarnings({"all"})
public static void main(String[] args) {
List list = new ArrayList();
// add:添加单个元素
list.add("jack");
list.add(10);//list.add(new Integer(10))
list.add(true);
System.out.println("list=" + list);
//remove:删除指定元素
//list.remove(0);//删除第一个元素
list.remove(true);//指定删除某个元素
System.out.println("list=" + list);
//contains:查找元素是否存在
System.out.println(list.contains("jack"));//T
//size:获取元素个数
System.out.println(list.size());//2
//isEmpty:判断是否为空
System.out.println(list.isEmpty());//F
//clear:清空
list.clear();
System.out.println("list=" + list);
//addAll:添加多个元素
ArrayList list2 = new ArrayList();
list2.add("红楼梦");
list2.add("三国演义");
list.addAll(list2);
System.out.println("list=" + list);
//containsAll:查找多个元素是否都存在
System.out.println(list.containsAll(list2));//T
//removeAll:删除多个元素
list.add("聊斋");
list.removeAll(list2);
System.out.println("list=" + list);//[聊斋]
}
}
Collection 接口遍历元素方式 1 使用 Iterator(迭代器) (lterator 仅用于遍历集合,lterator 本身并不存放对象
)(在调用iterator.next()方法之前必须要调用iterator.hasNext()进行检测
。若不调用,且下一条记录无效,直接调用it.next()会抛出NoSuchElementException异常)
1)lterator对象称为选代器,主要用于遍历 Collection 集合中的元素
。
2)所有实现了Collection接口的集合类都有一个iterator()方法,用以返回一个实现了lterator接口的对象,即可以返回一个选代器
。
3)lterator 的结构,[看一张图]
4)lterator 仅用于遍历集合,lterator 本身并不存放对象
提示:在调用iterator.next()方法之前必须要调用iterator.hasNext()进行检测。若不调用,且下一条记录无效,直接调用it.next()会抛出NoSuchElementException异常。
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
/**
* @创建人 wdl
* @创建时间 2024/9/6
* @描述
*/
public class CollectionIterator {
@SuppressWarnings({"all"})
public static void main(String[] args) {
Collection col = new ArrayList();
col.add(new Book("三国演义", "罗贯中", 10.1));
col.add(new Book("小李飞刀", "古龙", 5.1));
col.add(new Book("红楼梦", "曹雪芹", 34.6));
//System.out.println("col=" + col);
//现在老师希望能够遍历 col 集合
//1. 先得到 col 对应的 迭代器
Iterator iterator = col.iterator();
//2. 使用 while 循环遍历
while (iterator.hasNext()) {//判断是否还有数据
//返回下一个元素,类型是 Object
Object obj = iterator.next();
System.out.println("obj=" + obj);
}
//3. 当退出 while 循环后 , 这时 iterator 迭代器,指向最后的元素
// iterator.next();//NoSuchElementException
//4. 如果希望再次遍历,需要重置我们的迭代器
iterator = col.iterator();
System.out.println("===第二次遍历===");
while (iterator.hasNext()) {
Object obj = iterator.next();
System.out.println("obj=" + obj);
}
}
}
class Book {
private String name;
private String author;
private double price;
public Book(String name, String author, double price) {
this.name = name;
this.author = author;
this.price = price;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
@Override
public String toString() {
return "Book{" +
"name='" + name + '\'' +
", author='" + author + '\'' + ", price=" + price +
'}';
}
}
Collection 接口遍历对象方式 2 for 循环增强(增强for就是简化版的iterator本质一样
)
增强for循环,可以代替iterator迭代器,特点:增强for就是简化版的iterator本质一样
。只能用于遍历集合或数组。
基本语法
for(元素类型 元素名:集合名或数组名){
访问元素
}
/**
* @创建人 wdl
* @创建时间 2024/9/6
* @描述
*/
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class CollectionExercise {
@SuppressWarnings({"all"})
public static void main(String[] args) {
List list = new ArrayList();
list.add(new Dog("小黑", 3));
list.add(new Dog("大黄", 100));
list.add(new Dog("大壮", 8));
//先使用 for 增强
for (Object dog : list) {
System.out.println("dog=" + dog);
}
//使用迭代器
System.out.println("===使用迭代器来遍历===");
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
Object dog = iterator.next();
System.out.println("dog=" + dog);
}
}
}
/**
* 创建 3 个 Dog {name, age} 对象,放入到 ArrayList 中,赋给 List 引用
* 用迭代器和增强 for 循环两种方式来遍历
* 重写 Dog 的 toString 方法, 输出 name 和 age
*/
class Dog {
private String name;
private int age;
public Dog(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 "Dog{" +
"name='" + name + '\'' + ", age=" + age +
'}';
}
}
List 接口和常用方法(有序且可重复
,支持添加null,支持索引)(只能通过set方法进行修改)(下列所有方法对其实现类通用)
1)List集合类中元素有序
(即添加顺序和取出顺序一致)、且可重复
2)List集合中的每个元素都有其对应的顺序索引,即支持索引
。
3) List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素。
4) JDK API中List接口的实现类有:
增,boolean add(E e)
、void add(int index, E element)
、boolean addAll
(int index, Collection eles):从 index 位置开始将 eles 中的所有元素添加进来
删,Object remove(int index)
:移除指定 index 位置的元素,并返回此元素
,boolean remove(Object o)
;
改,Object set
(int index, Object ele):设置指定 index 位置的元素为 ele , 相当于是替换.
查,Object get
(int index):获取指定 index 位置的元素;int indexOf
(Object obj):返回 obj 在集合中首次出现的位置,int lastIndexOf(Object obj):返回 obj 在当前集合中末次出现的位置
插,void add(int index, E element)
截取,List subList
(int fromIndex, int toIndex):返回从 fromIndex 到 toIndex 位置的子集合 [fromIndex,toIndex)
其他 , Collection 接口中的方法
三种遍历方式(1.使用iterator 2.使用增强for 3.使用普通for)
ArrayList 底层结构和源码分析(由数组来实现数据存储的)(ArrayList 基本等同于Vector,但是其线程不安全(没有额外的同步机制)(执行效率高)
)(无参 0,10,1.5倍)(有参 指定,1.5倍)
- ArrayList中
维护了一个Object类型的数组elementData
.
ArrayList 实现了 Serializable 接口,所以它本身是可以序列化的,但ArrayList 的设计者选择将这个数组声明为 transient,这样在序列化时不会直接序列化整个数组,而是通过自定义的方法只序列化实际存储的元素,这样可以节省空间
transient Object[l elementData; //transient 表示瞬间,短暂的,表示该属性不会被序列化
- 当创建ArrayList对象时,如果使用的是无参构造器,则
初始elementData容量为0
,第1次添加,则扩容elementData为10
,如需要再次扩容,则扩容elementData为1.5倍
。 - 如果使用的是指定大小的构造器,则初始elementData容量为指定大小,如果需要扩容则直接扩容elementData为1.5倍。
Vector(用的少) 底层结构和源码分析(底层也是一个对象数组)(线程同步的,线程安全)(无参 10,2倍)(有参 指定,2倍)
- Vector类的定义说明
public class Vector<E>
extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
-
Vector底层也是一个对象数组
,
protected Object[]elementData;
- Vector 是
线程同步的,即线程安全
,Vector类的操作方法带有synchronized
public synchronized E get(int index) {
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
return elementData(index);
}
- 当创建Vector对象时,如果使用的是无参构造器,则
初始elementData容量为10
,如需要扩容,则扩容elementData为2倍
。 - 如果使用的是指定大小的构造器,则初始elementData容量为指定大小,如果需要扩容则直接扩容elementData为2倍。
import java.util.Vector;
/**
* @创建人 wdl
* @创建时间 2024/9/7
* @描述
*/
@SuppressWarnings({"all"})
public class Vector_ {
public static void main(String[] args) {
//无参构造器
//有参数的构造
Vector vector = new Vector(8);
for (int i = 0; i < 10; i++) {
vector.add(i);
}
vector.add(100);
System.out.println("vector=" + vector);
//解读源码
//1. new Vector() 底层
/*
public Vector() { this(10);
}
补充:如果是 Vector vector = new Vector(8);
走的方法:
public Vector(int initialCapacity) { this(initialCapacity, 0);
}
2.vector.add(i)
2.1//下面这个方法就添加数据到 vector 集合
public synchronized boolean add(E e) { modCount++; ensureCapacityHelper(elementCount + 1); elementData[elementCount++] = e;
return true;
}
2.2//确定是否需要扩容 条件 : minCapacity - elementData.length>0 private void ensureCapacityHelper(int minCapacity) {
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
2.3 //如果 需要的数组大小 不够用,就扩容 , 扩容的算法
//newCapacity = oldCapacity + ((capacityIncrement > 0) ?
// capacityIncrement : oldCapacity);
//就是扩容两倍.
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0) newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
*/
}
}
LinkedList 底层结构和源码分析(底层维护了一个双向链表
,而且维护了两个属性first和last分别指向首节点和尾节点,添加和删除效率较高)(线程不安全
(没有同步机制))
- LinkedList
底层维护了一个双向链表.
- LinkedList中维护了两个属性first和last分别指向 首节点和尾节点
- 每个节点(Node对象),里面又维护了
prev、next、item三个属性
,其中通过prev指向前一个,通过next指向后一个节点。最终实现双向链表. - 所以LinkedList的元素的添加和删除,不是通过数组完成的,相对来说效率较高
模拟一个简单的双向链表
/**
* @创建人 wdl
* @创建时间 2024/9/7
* @描述
*/
public class LinkedList01 {
public static void main(String[] args) {
//模拟一个简单的双向链表
Node jack = new Node("jack");
Node tom = new Node("tom");
Node hsp = new Node("老韩");
//连接三个结点,形成双向链表
//jack -> tom -> hsp
jack.next = tom;
tom.next = hsp;
//hsp -> tom -> jack
hsp.pre = tom;
tom.pre = jack;
Node first = jack;//让 first 引用指向 jack,就是双向链表的头结点
Node last = hsp; //让 last 引用指向 hsp,就是双向链表的尾结点
//演示,从头到尾进行遍历 System.out.println("===从头到尾进行遍历===");
while (true) {
if (first == null) {
break;
}
//输出 first 信息
System.out.println(first);
first = first.next;
}
//演示,从尾到头的遍历 System.out.println("====从尾到头的遍历====");
while (true) {
if (last == null) {
break;
}
//输出 last 信息
System.out.println(last);
last = last.pre;
}
//演示链表的添加对象/数据,是多么的方便
//要求,是在 tom --------- 老韩直接,插入一个对象 smith
//1. 先创建一个 Node 结点,name 就是 smith
Node smith = new Node("smith");
//下面就把 smith 加入到双向链表了
smith.next = hsp;
smith.pre = tom;
hsp.pre = smith;
tom.next = smith;
//让 first 再次指向 jack
first = jack;//让 first 引用指向 jack,就是双向链表的头结点
System.out.println("===从头到尾进行遍历===");
while (true) {
if (first == null) {
break;
}
//输出 first 信息
System.out.println(first);
first = first.next;
}
last = hsp; //让 last 重新指向最后一个结点
//演示,从尾到头的遍历
System.out.println("====从尾到头的遍历====");
while (true) {
if (last == null) {
break;
}
//输出 last 信息
System.out.println(last);
last = last.pre;
}
}
}
//定义一个 Node 类,Node 对象 表示双向链表的一个结点
class Node {
public Object item; //真正存放数据
public Node next; //指向后一个结点
public Node pre; //指向前一个结点
public Node(Object name) {
this.item = name;
}
@Override
public String toString() {
return "Node name=" + item;
}
}
Set 接口和常用方法(无序不可重复
,支持添加null( TreeSet 需要对元素进行排序,不能存储null),不支持索引
)(注意:添加的顺序和取出的顺序不一致,但是他是固定的
)(下列所有方法对其实现类通用)
和 List 接口一样, Set 接口也是 Collection 的子接口,因此,常用方法和 Collection 接口一样
增,boolean add(E e)
、、boolean addAll
(Collection eles):将 eles 中的所有元素添加进来
删,boolean remove(Object o)
;
其他 Collection 接口中的方法
两种遍历方式(1.使用iterator 2.使用增强for )(不能使用索引的方式来获取
)
HashSet 底层结构和源码分析(HashSet实际上是HashMap,HashMap底层是(数组+链表/红黑树))(0,16(看阈值,为其0.75,实际12时候要扩容),2倍)(与hashCode() 和 equals() 息息相关)(O(1))
public HashSet() {
map = new HashMap<>();
}
添加元素原理(附面试题)
- 添加一个元素时,先得到
hash值
一会转成->索引值
- 找到存储数据表table,看这个索引位置是否已经存放的有元素
- 如果没有,直接加入
- 如果有 , 调用 equals 比较逐个比较,如果有相同,就放弃添加,全部比较完都不相同,则添加到最后
经典面试问题:字符串加入 HashSet(String 类已经重写了 hashCode() 和 equals() 方法,两个内容相同的字符串对象被认为是相等的)
set.add(new String("hsp")); // ok
set.add(new String("hsp")); // won't add
String 类已经重写了 hashCode() 和 equals() 方法,两个内容相同的字符串对象被认为是相等的。因此,虽然两个 String 对象是不同的实例,但由于它们的值相同,所以第二次添加 “hsp” 时,HashSet 判断它已经存在,因此不会再次添加。
扩容机制(太大了转化成红黑树)
- HashSet底层是HashMap,
第一次添加时,table 数组扩容到 16
,临界值(threshold)是 16*加载因子(loadFactor)是0.75 =12 - 如果table 数组使用到了临界值 12,就会扩容到 16
*
2 =32,新的临界值就是32*0.75 =24,依次类推
转成红黑树机制
注意:在Java8中,如果一条链表的元素个数到达
TREEIFY _THRESHOLD(默认是8
),并且
table的大小
>=MIN_TREEIFY_CAPACITY(默认是64
)就会进行树化(红黑树),否则仍然采用数组扩容机制
LinkedHashSet(是 HashSet 的子类)底层结构和源码分析(底层是一个 LinkedHashMap,底层维护了一个 数组+链表
)(O(1))(有序,因为其多维护了一个和其插入顺序有关的双向链表)
TreeSet底层结构和源码分析(底层是一个 TreeMap,底层维护了一个 红黑树
)(O(logn))( TreeSet 需要对元素进行排序,不能存储null
)
TreeSet treeSet = new TreeSet(new Comparator() { @Override
public int compare(Object o1, Object o2) {
//下面 调用 String 的 compareTo 方法进行字符串大小比较
//如果老韩要求加入的元素,按照长度大小排序
//return ((String) o2).compareTo((String) o1);
return ((String) o1).length() - ((String) o2).length();
}
});
Map 接口和常用方法(key 不允许重复(当有相同的 k , 就等价于替换), value 可以重复)(key 和 value 之间存在单向一对一关系)(常用String类作为Map的 key)
增,puk
(key,value)
map.put("邓超", new Book("", 100));
删,V remove
(Object key):移除指定键key的元素,并返回此元素的value
改,puk
(key,value),修改键为key的元素
查,V get
(Object key):获取键为key的value值;V getOrDefault
(Object key, V defaultValue) ;boolean containsKey
(Object key);判断键key是否存在;keySet
:获取所有的键;values
:获取所有的值;entrySet
:获取所有关系k-v(Set<Map.Entry<K, V>> entrySet();需要向下类型转化为Map.Entry)
for (Object entry : entrySet) {
//将 entry 转成 Map.Entry
Map.Entry m = (Map.Entry) entry;
System.out.println(m.getKey() + "-" + m.getValue());
}
//(2) 迭代器
System.out.println("----使用 EntrySet 的 迭代器(第 4 种) ");
Iterator iterator3 = entrySet.iterator();
while (iterator3.hasNext()) {
Object entry = iterator3.next();
//System.out.println(next.getClass());//HashMap$Node -实现-> Map.Entry (getKey,getValue)
//向下转型 Map.Entry
Map.Entry m = (Map.Entry) entry;
System.out.println(m.getKey() + "-" + m.getValue());
}
大小容量 size,isEmpty
清空 clear
两种遍历方式(1.使用iterator 2.使用增强for)
HashMap 底层结构和源码分析(不保证映射的顺序,因为底层是以hash表的方式来存储的)(jdk7的hashMap 底层 数组+链表,jdk8的hashMap 底层 数组+链表/红黑树)(线程不安全自,方法没有做同步互斥的操作,没有synchronized)(注意:是整个 HashMap 都变为红黑树:
只有冲突较多的链表才会被转换为红黑树`,而不是整个 HashMap)(扩容机制等见HashSet)
- HashMap底层维护了
Node类型的数组table
,默认为null
- 当添加key-val时,通过key的哈希值(hashcode)得到在table的索引。然后判断该索引处是否有元素,如果没有元素直接添加。如果该索引处有元素(链表/红黑树),继续判断该元素的key和准备加入的key相是否相等(equals),如果相等,则直接替换val;如果不相等需要判断是树结构还是链表结构,做出相应处理。如果添加时发现容量不够,则需要扩容。
LinkedHashMap(是 HashMap 的子类)底层结构和源码分析(底层维护了一个 数组+链表
)(O(1))(有序,因为其多维护了一个和其插入顺序有关的双向链表)
TreeMap 底层结构和源码分析(底层维护了一个 红黑树
)(O(logn))( TreeSet 需要对元素进行排序,不能存储key为null的元素
)
TreeMap treeMap = new TreeMap(new Comparator() { @Override
public int compare(Object o1, Object o2) {
//按照传入的 k(String) 的大小进行排序
//按照 K(String) 的长度大小排序
//return ((String) o2).compareTo((String) o1);
return ((String) o2).length() - ((String) o1).length();
}
});
HashTable 底层结构和源码分析(键和值都不能为null
)(线程安全的synchronized)
Properties(是 HashTable 的子类) 底层结构和源码分析(还可以用于 从 xxx.properties 文件中,加载数据到Properties类对象,并进行读取和修改)
开发中如何选择集合实现类(记住)
- 先判断存储的类型(一组对象[单列]或一组键值对[双列])
- 一组对象[单列]:Collection接口
- 允许重复:List
增删多:LinkedList[底层维护了一个双向链表]
改查多:ArrayList[底层维护 Object类型的可变数组] - 不允许重复:Set
无序:HashSet[底层是HashMap ,维护了一个哈希表 即(数组+链表+红黑树)]
有序:LinkedHashSet,维护数组+双向链表
排序:TreeSet
- 一组键值对[双列]:Map
- 键无序:HashMap[底层是:哈希表 jdk7:数组+链表,jdk8: 数组+链表+红黑树]
- 键有序:LinkedHashMap
- 键排序:TreeMap
- 读取文件:Properties
Collections 工具类
(注意:不是Collection接口
)(里面的方法均为static 方法)
1)Collections 是一个操作 Set、List 和 Map 等集合的工具类
2)Collections 中提供了一系列静态的方法
对集合元素进行排序、查询和修改等操作
排序, sort(List);sort(List,Comparator)
Collections.sort(list);
Collections.sort(list, new Comparator() { @Override
public int compare(Object o1, Object o2) {
//可以加入校验代码.
return ((String) o2).length() - ((String) o1).length();
}
});
翻转, reverse
指定元素的出现次数 ,frequency(Collection,Object)
交换, swap(List,int, int):将指定 list 集合中的 i 处元素和 j 处元素进行交换
最大/小元素, max(Collection);max(Collection,Comparator)
复制,copy(List dest,List src)
替换,replaceAll(List list,Object oldVal,Object newVal)
本章练习题
1. 放入TreeSet里面的元素一定是可以比较的(内部通过比较来确保元素的顺序和去重,因此无法比较的元素会导致ClassCastException异常)
import java.util.TreeSet;
class Person {
private String name;
public Person(String name) {
this.name = name;
}
// Getter for name
public String getName() {
return name;
}
}
public class Homework01 {
public static void main(String[] args) {
TreeSet<Person> treeSet = new TreeSet<>();
// 试图添加Person对象,但由于Person没有实现Comparable接口,运行时会抛出ClassCastException
treeSet.add(new Person("John")); // 会抛出异常
}
}
2.HashSet里面放入对象的常见陷阱