第十二章、 集合
12.1 集合的理解和好处
- 数组
(1). 长度开始时必须指定,而且一旦指定,不能修改
(2). 保存的必须为同一类型的元素
(3). 使用数组进行增加/删除元素的示意代码—比较麻烦
假如要写一个Person对象数组扩容:
//原数组
Person[] per=new Person[1];
per[0]=new Person();
//扩容后
Person per2=new Person[per.length+1];
for{}//拷贝per数组的元素到per2中
per2[per.length-1]=new Person();//添加新对象
- 集合
(1). 可以动态保存任意多个对象,使用比较方便!
(2). 提供了一系列方便操作对象的方法:add、remove、set、get等
(3). 使用集合添加,删除新元素的示意代码-简洁明了
12.2 集合框架体系
Java的集合类很多,主要分为两大类,如下图:【要求背下来】
小结:
(1). 集合主要是两组(单列集合、双列集合)
(2). Collection 接口有两个重要的子接口 List Set ,他们的实现子类都是单列集合
(3). Map 接口的实现子类 是双列集合,存放的K-V
package chapter12.collection_;
import java.util.*;
/**
* @aim java基础学习
* @note java笔记
*/
public class Collection_ {
@SuppressWarnings({"all"})
public static void main(String[] args) {
//单列集合
ArrayList arrayList = new ArrayList();
arrayList.add("mark");
arrayList.add("smath");
//双列集合
HashMap hashMap = new HashMap();
hashMap.put("1", "小明");
hashMap.put("2", "小红");
}
}
12.3 Collection
12.3.1 Collection接口和常用方法
- Collection接口实现类的特点
public interface Collection<E> extends Iterable<E>
(1). Collection实现子类可以存放多个元素,每个元素可以是Object
(2). 有些Collection的实现类,可以存放重复的元素,有些是不可以
(3). 有些Collection的实现类,有些是有序的(List),有些不是有序的(Set)
(4). Collection接口没有直接的实现子类,是通过它的子接口Set和List来实现的
- Collection接口常用方法,已实现子类.(由于接口不能实例化,用实现类ArrayList演示)
(1). add:添加单个元素
(2). remove:删除指定元素
(3). contains:查找元素是否存在
(4). size:获取元素个数
(5). isEmpty:判断是否为空
(6). clear:清空
(7). addAll:添加多个元素
(8). containsAll:查找多个元素是否都存在
(9). removeAll:删除多个元素
(10). 说明:以ArrayList实现类来演示
package chapter12.collection_;
import java.util.ArrayList;
import java.util.Collection;
/**
* @aim java基础学习
* @note java笔记
*/
public class CollectionMethod_ {
@SuppressWarnings({"all"})
public static void main(String[] args) {
Collection col=new ArrayList();
//(1). add:添加单个元素
col.add("hello");
col.add(1000);//本质:col.add(new Integer(1000));
col.add(false);
col.add('h');
col.add(new Integer(100));
col.add("hello");
System.out.println(col);//[hello, 100, false, h, 100, hello]
//(2). remove:删除指定元素,删除找到的第一个元素
col.remove("hello");
System.out.println(col);//[100, false, h, 100, hello]
//(3). contains:查找元素是否存在
boolean contains=col.contains(100);
System.out.println(contains);//true
//(4). size:获取元素个数
System.out.println(col.size());//5
//(5). isEmpty:判断是否为空
System.out.println(col.isEmpty());//false
//(6). clear:清空
col.clear();
System.out.println(col);//[]
//(7). addAll:添加多个元素
Collection co1=new ArrayList();
Collection co2=new ArrayList();
co1.add("A");
co1.add("B");
co1.add("C");
co2.addAll(co1);
System.out.println(co2);//[A, B, C]
//(8). containsAll:查找多个元素是否都存在
co2.add("D");
boolean contain=co2.containsAll(co1);
System.out.println(contain);//true
//(9). removeAll:删除多个元素
co2.removeAll(co1);
System.out.println(co2);//[D]
}
}
12.3.2 Collection接口遍历元素方式1-使用Iterator(迭代器)
- Iterator基本介绍
(1). Iterator对象称为迭代器,主要用于遍历Collection集合中的元素。
(2). 所有实现了Collection接口的集合类都有一个iterator()方法,用以返回一个实现了Iterator接口的对象,即可以返回一个迭代器。
(3). Iterator仅用于遍历集合,Iterator本身并不存放对象。
(4). 迭代器的执行原理:
Collection coll=new ArrayList();
Iterator iterator=coll.iterator();//得到一个迭代器
//hashNext():判断是否还有下一个元素
while(iterator.hasNext()){
iterator.next();//next():作用:1.指针下移 2.将下移后集合上的元素返回
System.out.println(iterator.next());
}
- Iterator 接口主要有以下三个方法:
(1). boolean hasNext(); 检查集合中是否还有下一个元素。
(2). E next(); 返回集合中的下一个元素。
(3). void remove(); 从集合中移除 next() 方法返回的最后一个元素。这个方法是可选的,某些实现可能不支持这个操作。
(4). 下面是一个使用 Iterator 的示例:
package chapter12.collection_;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
/**
* @aim java基础学习
* @note java笔记
*/
public class IteratorExample {
@SuppressWarnings({"all"})
public static void main(String[] args) {
// 创建一个ArrayList并添加一些元素
Collection coll = new ArrayList();
coll.add("苹果");
coll.add("香蕉");
coll.add("草莓");
// 获取coll 对应的Iterator对象
Iterator iterator = coll.iterator();
// 遍历集合
// 这里有个快捷键,快速生成while =>itit
// 显示所有快捷键的快捷键 ctrl + j
while (iterator.hasNext()) {
// 获取下一个元素,类型是Object
Object obj=iterator.next();
System.out.println(obj);
// 如果需要移除某些元素,可以使用remove方法
// iterator.remove(); // 可选操作
}
}
}
/*
苹果
香蕉
草莓
*/
-
注意事项
(1). hasNext() 方法在集合的元素遍历完毕后返回 false。
(2). next() 方法在集合遍历到最后一个元素时必须调用 hasNext() 来确保还有更多的元素,否则会抛出 NoSuchElementException。
(3). remove() 方法在调用 next() 之后才有效,且在调用 remove() 之前不能再调用 next() 方法,否则会抛出 IllegalStateException。
(4). 当退出while循环后,这时iterator迭代器,指向最后的元素,此时如果再使用next()方法就会报错NoSuchElementException。如果希望再次遍历,就需要重置一下迭代器即可。即再次获取coll 对应的Iterator对象。iterator = coll.iterator(); -
小结:
在调用iterator.next();方法之前必须要调用iterator.hasNext();进行检测。若不调用,且下一条记录无效,直接调用iterator.next()的话,就会抛出NoSuchElementException。
12.3.3 Collection接口遍历元素方式2-使用for循环增强。
增强for循环,可以替代Iterator迭代器,特点:增强for就是简化的Iterator,本质是一样的。只能用于遍历集合或数组。
- 基本语法
for(元素类型 元素名:集合名或数组名){
访问元素;
}
package chapter12.collection_;
import java.util.ArrayList;
import java.util.Collection;
/**
* @aim java基础学习
* @note java笔记
*/
public class CollectionFor_ {
@SuppressWarnings({"all"})
public static void main(String[] args) {
Collection coll = new ArrayList();
coll.add("苹果");
coll.add("香蕉");
coll.add("草莓");
//增强for循环,底层仍然是通过迭代器去实现的
//快捷键 I
for (Object str:coll) {
System.out.println(str);
}
}
}
/*
苹果
香蕉
草莓
*/
12.3.4 Collection练习
- 编写程序
(1). 创建 3个Dog{name,age}对象,放入到ArrayList中,赋给List引用
(2). 用迭代器和增强for循环两个方式来遍历
(3). 重写Dog的toString方法,输出name和age
package chapter12.collection_;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* @aim java基础学习
* @note java笔记
*/
public class CollectionExercise {
public static void main(String[] args) {
Dog dog1 = new Dog("小黑", 2);
Dog dog2 = new Dog("小黄", 3);
Dog dog3 = new Dog("小白", 5);
List arrayList=new ArrayList();
arrayList.add(dog1);
arrayList.add(dog2);
arrayList.add(dog3);
//迭代器
Iterator iterator = arrayList.iterator();
while(iterator.hasNext()){
Object obj= iterator.next();
System.out.println(obj);
}
//Dog{name='小黑', age=2}
//Dog{name='小黄', age=3}
//Dog{name='小白', age=5}
//增强for
for (Object o :arrayList) {
System.out.println(o);
}
//Dog{name='小黑', age=2}
//Dog{name='小黄', age=3}
//Dog{name='小白', age=5}
}
}
class Dog{
private String name;
private int age;
public Dog(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
12.3.5 List接口和常用方法
- List接口基本介绍
(1). List接口是Collection接口的子接口。
(2). List集合类中元素有序(即添加顺序和取出顺序一致)、且可重复
(3). List集合类中每个元素都有其对应的顺序索引,即支持索引。
(4). List容器中的元素都对应一个整数型的序号记载器在容器中的位置,可以根据序号存取容器中的元素。
(5). JDK API中List接口的实现类有:
其中常用的有ArrayList、LinkedList和Vector
package chapter12.list_;
import java.util.ArrayList;
import java.util.List;
/**
* @aim java基础学习
* @note java笔记
*/
public class List_ {
@SuppressWarnings({"all"})
public static void main(String[] args) {
//(1). List接口是Collection接口的子接口。
//(2). List集合类中元素有序(即添加顺序和取出顺序一致)、且可重复
List list=new ArrayList();
list.add("123");
list.add("234");
list.add("345");
list.add("456");
//(3). List集合类中每个元素都有其对应的顺序索引,即支持索引。
//(4). List容器中的元素都对应一个整数型的序号记载器在容器中的位置,可以根据序号存取容器中的元素。
System.out.println(list.get(3));//456
}
}
- List接口的常用方法
List集合里添加了一些根据索引来操作集合元素的方法
(1). void add(int index,Object ele): 在index位置插入ele元素
(2). boolean addAll(int index,Collection else):从index位置开始将eles中的所有元素添加进来
(3). Object get(int index):获取指定index位置的元素
(4). int indexOf(Object obj):返回obj在集合中首次出现的位置
(5). int lastIndexOf(Object obj):返回obj在当前集合中末次出现的位置
(6). Object remove(int index):移除指定index位置的元素,并返回此true
(7). Object set(int index,Object ele):设置指定index位置的元素为ele,相当于是替换
(8). List subList(int fromIndex,int toIndex):返回从fromIndex到toIndex位置的子集合 ,左闭右开
package chapter12.list_;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* @aim java基础学习
* @note java笔记
*/
public class ListMethod {
@SuppressWarnings({"all"})
public static void main(String[] args) {
List list = new ArrayList();
list.add("金角");
list.add("银角");
System.out.println(list);//[金角, 银角]
//(1). void add(int index,Object ele):
list.add(1,"到");
System.out.println(list);//[金角, 到, 银角]
//(2). boolean addAll(int index,Collection else):从index位置开始将eles中的所有元素添加进来
Collection coll=new ArrayList();
coll.add("黑白");
coll.add("到");
list.addAll(2,coll);
System.out.println(list);//[金角, 到, 黑白, 到, 银角]
//(3). Object get(int index):获取指定index位置的元素
System.out.println(list.get(2));//黑白
//(4). int indexOf(Object obj):返回obj在集合中首次出现的位置
System.out.println(list.indexOf("到"));//1
//(5). int lastIndexOf(Object obj):返回obj在当前集合中末次出现的位置
System.out.println(list.lastIndexOf("到"));//3
//(6). Object remove(int index):移除指定index位置的元素,并返回true
boolean b=list.remove("到");
System.out.println(b);//true
System.out.println(list);//[金角, 黑白, 到, 银角]
//(7). Object set(int index,Object ele):设置指定index位置的元素为ele,相当于是替换
list.set(1,"到");
System.out.println(list);//[金角, 到, 到, 银角]
//(8). List subList(int fromIndex,int toIndex):返回从fromIndex到toIndex位置的子集合,左闭右开
List list1=list.subList(2,4);
System.out.println(list1);//[到, 银角]
}
}
12.3.6 List的三种遍历方式【ArrayList、LinkedList、Vector】
- 因为ArrayList、LinkedList、Vector是List的实现类,所以这三种遍历方式都是通用的。
package chapter12.list_;
import java.util.*;
/**
* @aim java基础学习
* @note java笔记
*/
public class ListTraversal {
@SuppressWarnings({"all"})
public static void main(String[] args) {
// List list=new ArrayList();
// List list=new LinkedList();
List list=new Vector();
list.add("Dog");
list.add("Cat");
list.add("Red");
list.add("Blue");
list.add("Green");
//使用Iterator 遍历
Iterator iterator= list.iterator();
while(iterator.hasNext()){
Object o=iterator.next();
System.out.print(o+" ");
}
System.out.println();
//使用增强for 快捷键 输入I
for (Object o :list) {
System.out.print(o+" ");
}
System.out.println();
//普通for 快捷键 输入fori
for(int i=0;i<list.size();i++){
System.out.print(list.get(i)+" ");
}
}
}
/*
Dog Cat Red Blue Green
Dog Cat Red Blue Green
Dog Cat Red Blue Green
*/
12.3.7 List接口练习
- 添加10个以上的元素(比如String “hello”),在2号位插入一个元素“Jack”,获得第5个元素,删除第六个元素,修改第7个元素,在使用迭代器遍历集合,要求:使用List的实现类ArrayList完成。
package chapter12.list_;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* @aim java基础学习
* @note java笔记
*/
public class ListExercise {
@SuppressWarnings({"all"})
public static void main(String[] args) {
List list=new ArrayList();
//1.添加10个hello
for (int i = 0; i < 10; i++) {
list.add("hello");
}
System.out.println(list);//[hello, hello, hello, hello, hello, hello, hello, hello, hello, hello]
//2.在2号位添加一个Jack
list.add(1,"jack");
System.out.println(list);//[hello, jack, hello, hello, hello, hello, hello, hello, hello, hello, hello]
//3.获取第五个元素
String fifth=(String) list.get(4);
System.out.println(fifth);//hello
//4.删除第六个元素
list.remove(5);
System.out.println(list);//[hello, jack, hello, hello, hello, hello, hello, hello, hello, hello]
//5.修改第七个元素
list.set(6,"mark");
//6.通过迭代器遍历集合
Iterator iterator=list.iterator();
while(iterator.hasNext()) {
Object o=iterator.next();
System.out.print(o+" ");
}
//hello jack hello hello hello hello mark hello hello hello
}
}
- 使用List的实现类添加三本图书,并遍历,打印如下效果:
名称:xx 价格:xx 作者:xx
名称:xx 价格:xx 作者:xx
名称:xx 价格:xx 作者:xx
(1). 按价格排序,从低到高(使用冒泡排序)
(2). 要求使用ArrayList、LinkedList、Vector三种集合实现
(3). 结论:主要说明,只要实现了List接口,那么List的实现类都可以使用List接口中的方法。
- ListExercise02类
package chapter12.list_;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Vector;
/**
* @aim java基础学习
* @note java笔记
*/
@SuppressWarnings({"all"})
public class ListExercise02 {
public static void main(String[] args) {
List arrayList = new ArrayList();
// List linkedList=new LinkedList();
// List vector=new Vector();
arrayList.add(new Book("水浒传", 120, "施耐庵"));
arrayList.add(new Book("三国演义", 80, "罗贯中"));
arrayList.add(new Book("西游记", 160, "吴承恩"));
arrayList.add(new Book("红楼梦", 200, "曹雪芹"));
System.out.println(arrayList.get(0).getClass());
System.out.println();
for (Object o : arrayList) {
System.out.println(o);
}
System.out.println("======排序后======");
bubbleSort(arrayList);
for (Object o : arrayList) {
System.out.println(o);
}
}
public static void bubbleSort(List list) {
int listSize = list.size();
for (int i = 0; i < listSize - 1; i++) {
for (int j = 0; j < listSize - i - 1; j++) {
Book b1 = (Book) list.get(j);
Book b2 = (Book) list.get(j + 1);
if (b1.getPrice() > b2.getPrice()) {
list.set(j + 1, b1);
list.set(j, b2);
}
}
}
}
}
- Book类
package chapter12.list_;
/**
* @aim java基础学习
* @note java笔记
*/
public class Book {
private String name;
private double price;
private String author;
public Book(String name, double price, String author) {
this.name = name;
this.price = price;
this.author = author;
}
public double getPrice() {
return price;
}
@Override
public String toString() {
return "名称:" + name + "\t\t价格:" + price + "\t\t作者:" + author + "\t\t";
}
}
12.3.8 ArraysList底层结构和源码分析
- ArrayList的注意事项
(1). Permits all elements,including null,ArrayList 可以假如null,并且多个
(2). ArrayList 是由数组来实现存储的
(3). ArrayList基本等同于Vector,除了ArrayList是线程不安全(执行效率高)。在多线程情况下,不建议使用ArrayList。
package chapter12.list_;
import java.util.ArrayList;
/**
* @aim java基础学习
* @note java笔记
*/
public class ArrayListDetail {
public static void main(String[] args) {
//ArrayList 是线程不安全的。源码中没有synchronized关键字
/*
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
*/
ArrayList arrayList=new ArrayList();
arrayList.add(null);
arrayList.add("hello");
arrayList.add(null);
System.out.println(arrayList);
}
}
注意:IDEA默认情况下,Debug 显示的数据是简化后的,如果希望看到完整的数据,需要做设置。如下图:
这是没有修改过看的数据:
这是修改过后看到的数据:
这是修改的地方:
2. ArrayList底层操作机制和源码分析(重点、难点)
(1). ArrayList中维护了一个Object类型的数组elementData.【debug看源码】
transient Object[] elementData;//transient 表示瞬间,短暂的,表示该属性不会被序列化。
(2). 当创建ArrayList对象时,如果使用的是无参构造器,则初始elementData容量为0(JDK7是10)
(3). 当添加元素时:先判断是否需要扩容,如果需要扩容,则调用grow方法,否则直接添加元素到适合位置。
(4). 如果使用的是无参构造器,如果第一次添加,需要扩容的话,则扩容elementData为10,如果需要再次扩容的话,则扩容elementData为1.5倍。
(5). 如果使用的是指定容量capacity的构造器,则初始elementData容量为capacity,如果需要扩容,则扩容elementData为1.5倍。
具体要进行Debug代码如下:
package chapter12.list_;
import java.util.ArrayList;
/**
* @aim java基础学习
* @note java笔记
*/
public class ArrayListSource {
@SuppressWarnings({"all"})
public static void main(String[] args) {
// ArrayList list =new ArrayList();
ArrayList list =new ArrayList(8);
for (int i = 1; i <= 10; i++) {
list.add(i);
}
for (int i = 11; i < 15; i++) {
list.add(i);
}
list.add(100);
list.add(200);
list.add(null);
for (Object o :list) {
System.out.print(o+" ");
}
}
}
Debug流程(无参构造器):
Debug流程(有参构造器):
12.3.9 Vector底层结构和源码分析
- Vector的基本介绍
(1). Vector类的定义说明
public class Vector<E>
extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
(2). Vector底层也是一个对象数组,protected Object[] elementData;
(3). Vector是线程同步的,即线程安全,Vector类的操作方法带有synchronized
public synchronized E get(int index) {
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
return elementData(index);
}
(4). 在开发中,需要线程同步安全时,考虑使用Vector
- Vector和ArrayList的比较
底层结构 | 最早出现版本 | 线程安全(同步)效率 | 扩容倍数 | |
---|---|---|---|---|
ArrayList | 可变数组 | Jdk1.2 | 不安全,效率高 | 如果有参构造1.5倍如果是无参:1.第一次102.从第二次开始按1.5倍扩容 |
Vector | 可变数组 | Jdk1.1 | 安全,效率不高 | 如果是无参,默认是10,满后按2倍扩容如果是指定大小,则每次直接按2倍扩容 |
package chapter12.list_;
import java.util.Vector;
/**
* @aim java基础学习
* @note java笔记
*/
public class Vector_ {
@SuppressWarnings({"all"})
public static void main(String[] args) {
Vector vector=new Vector();
// Vector vector=new Vector(9);
for (int i = 1; i < 11; i++) {
vector.add(i);
}
vector.add(100);
System.out.println(vector);
//1.创建Vector对象,初始化空间为10
/*
public Vector() {
this(10);
}
*/
//2.下面这个代码将数据添加到Vector集合里面去
/*
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
elementCount = s + 1;
}
里面的if条件判断语句就已经进行了是否需要扩容的判断,条件是s == elementData.length
*/
//3.如果数组大小不够用,就进行扩容,扩容算法如下:capacityIncrement为0;
/*
private int newCapacity(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
if (newCapacity - minCapacity <= 0) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return minCapacity;
}
return (newCapacity - MAX_ARRAY_SIZE <= 0)
? newCapacity
: hugeCapacity(minCapacity);
}
*/
//有参构造器,自己Debug追一下即可
}
}
12.3.10 LinkedList底层结构和源码分析
-
LinkedList的全面说明
(1). LinkedList实现了双向链表和双端队列特点
(2). 可以添加任意元素(元素可以重复),包括null
(3). 线程不安全,没有实现同步 -
LinkedList的底层操作机制
(1). LinkedList底层维护了一个双向链表
(2). LinkedList中维护了两个属性first和last分别指向 首节点和尾节点
(3). 每个节点(Node对象),里面又维护了prev、next、item三个属性,其中通过prev指向前一个,通过next指向后一个节点。最终实现双向链表
(4). 所以LinkedList的元素的添加和删除,不是通过数组完成的,相对来说效率较高。
(5). 模拟一个简单的双向链表
package chapter12.list_;
/**
* @aim java基础学习
* @note java笔记
*/
public class LinkedList01 {
public static void main(String[] args) {
Node jack = new Node("jack");
Node mark = new Node("mark");
Node smith = new Node("smith");
//连接三个节点,形成双向链表
//jack -> mark -> smith
jack.next=mark;
mark.next=smith;
//smith -> mark -> jack
smith.pre=mark;
mark.pre=jack;
Node first=jack;//让first引用指向Jack,就是双向链表的头结点
Node last=smith;//让last引用指向smith,就是双向链表的尾结点
System.out.println("===从头开始遍历输出===");
//从头开始遍历输出
while(true){
if(first==null){
break;
}
System.out.println(first);
first=first.next;
}
System.out.println("===从尾到头遍历===");
//从尾到头遍历
while(true){
if(last==null){
break;
}
System.out.println(last);
last=last.pre;
}
//现要求:在mark 后面插入一个hello对象
//1.首先创建hello对象
Node hello = new Node("hello");
hello.next=smith;
hello.pre=mark;
mark.next=hello;
smith.pre=hello;
first=jack;//让first重新引用指向Jack,就是双向链表的头结点,因为上面first已经移到到最后的smith去了
System.out.println("===插入hello后的链表===");
while(true){
if(first==null){
break;
}
System.out.println(first);
first=first.next;
}
}
}
class Node{
public Object item;//真正存放数据
public Node next;//指向后一个节点
public Node pre;//指向前一个节点
public Node(Object item) {
this.item = item;
}
@Override
public String toString() {
return "Node item= " + item;
}
}
- LinkedList源码分析
package chapter12.list_;
import java.util.LinkedList;
/**
* @aim java基础学习
* @note java笔记
*/
@SuppressWarnings({"all"})
public class LinkedListuse {
public static void main(String[] args) {
LinkedList linkedList=new LinkedList();
for (int i = 0; i < 2; i++) {
linkedList.add(i);
}
/*通过debug演示添加两个节点的源码以及分析
第一步:执行LinkedList linkedList=new LinkedList();进行初始化操作,first=null,last=null
public LinkedList() {}
第二步:执行for循环add(0),先是进行装箱,然后再执行添加操作
public boolean add(E e) {
linkLast(e);
return true;
}
第三部:执行linkLast(e),将创建双向链表的第一个节点,item=0,first=null,last=null,l是pre,null是next
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++;
}
第四步:执行for循环add(1),先是进行装箱,然后再执行添加操作,代码和第三步执行的一样。
不过条件不一样,执行的结果也不一样。通过和代码去分析即可。
这里做一个简单的分析:首先将@112这个节点的last指向赋给l,那么l也就指向了@112这个节点,
然后new一个新节点,因为l传参的位置是pre,所以这个新节点@122的pre就指向@112
其次,l!=null,执行的是l.next.l由上面知道是@112,那么@112的next就指向@122
*/
linkedList.add(100);
linkedList.add(200);
linkedList.add(300);
linkedList.add("hello");
System.out.println(linkedList);//[0, 1, 100, 200, 300, hello]
linkedList.remove();//默认删除第一个节点
System.out.println(linkedList);//[1, 100, 200, 300, hello]
linkedList.remove(3);//删除指定索引节点,索引从0开始
System.out.println(linkedList);//[1, 100, 200, hello]
linkedList.remove("hello");//删除指定内容节点
System.out.println(linkedList);//[1, 100, 200]
/*通过debug演示默认删除第一个节点的源码以及分析linkedList.remove();
第一步: public E remove() {
return removeFirst();
}
第二步: public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
让f也指向第一个节点,判断第一个节点是否存在,若不抛出异常,否则进行删除return unlinkFirst(f);
第三步: private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
final E element = f.item;//把第一个节点的值item赋给:element
final Node<E> next = f.next;//将第一个节点(@112)赋给next(@122),也就是next(@122)指向了第二个节点
f.item = null;//再把第一个节点的item和next置为null
f.next = null; // help GC//资源回收
first = next;//然后将first指向第二个节点,第二个节点(@122)就变成了第一个节点
if (next == null)
last = null;
else
next.prev = null;//将已经变成了第一个节点@122的pre置为null
size--;
modCount++;
return element;
}
*/
//修改某个节点的对象
linkedList.set(0,"hello");
System.out.println(linkedList);//[hello, 100, 200]
Object object=linkedList.get(1);//获取某个索引节点的对象
System.out.println(object);//100
System.out.println(linkedList.getFirst());//获取第一个节点,hello
System.out.println(linkedList.getLast());//获取最后一个节点,200
//因为LinkedList是实现了List接口,所以遍历方法也有三种:迭代器、增强for、普通for,这里就不写了
}
}
4. ArrayList和LinkedList的比较
底层结构 | 增删的效率 | 改查的效率 | |
---|---|---|---|
ArrayList | 可变数组 | 较低、数组扩容 | 较高 |
LinkedList | 双向链表 | 较高、通过链表追加 | 较低 |
如何选择ArrayList和LinkedList
(1). 如果需要改查的操作多,选择ArrayList
(2). 如果需要增删的操作多,选择LinkedList
(3). 一般来说,在程序中,80%~90%都是查询,因此大部分情况下会选择ArrayList
(4). 在一个项目中,根据业务灵活选择,也可能这样:一个模块使用的是ArrayList,另一个模块使用的是LinkedList,也就是说,要根据业务来进行选择
12.3.11 Set
- Set接口基本介绍
(1). 无序(添加和取出的顺序不一致),没有索引
(2). 不允许重复元素,所以最多包含一个null
(3). JDK API中Set接口的实现类有:
- Set接口的常用方法
和List接口一样,Set接口也是Collection的子接口,因此,常用方法和Collection接口一样。 - Set接口的变量方法
(1). 同Collection的遍历方式一样,因为Set接口是Collection接口的子接口。
(2). 可以使用迭代器和增强for循环,但是由于没有索引不能使用普通for循环
package chapter12.set_;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
/**
* @aim java基础学习
* @note java笔记
*/
@SuppressWarnings({"all"})
public class MapMethod {
public static void main(String[] args) {
//1.用Set的实现类HashSet,演示用的是Set的方法。
//2.set接口的实现类的对象(Set接口对象),不能存放重复的元素,只能添加一个
//3.set接口对象存放数据是无序的(即添加顺序和取出顺序不一致)
//4.注意:取出的顺序虽然不是添加的顺序,但是它的顺序是固定的
Set set = new HashSet();
set.add("jack");
set.add("mark");
set.add("smith");
set.add("smith");
set.add("john");
set.add(null);
//使用迭代器输出
Iterator iterator = set.iterator();
while (iterator.hasNext()) {
Object o = iterator.next();
System.out.print(o + " ");
}
System.out.println();
//使用增强for循环
for (Object o : set) {
System.out.print(o + " ");
}
/*
null smith john jack mark
null smith john jack mark
*/
}
}
12.3.12 Set 接口实现类-HashSet
- HashSet的全面说明
(1). HashSet实现了Set接口
(2). HashSet实际上是HashMap,看源码
public HashSet() {
map = new HashMap<>();
}
(3). 可以存放null值,但是只能有一个null
(4). HashSet不保证元素是有序的,取决于hash后,再确定索引的结果
(5). 不能有重复元素/对象(难点,要解析其源码怎么定义这个重复)。如下代码:
package chapter12.set_;
import java.util.HashMap;
import java.util.HashSet;
/**
* @aim java基础学习
* @note java笔记
*/
@SuppressWarnings({"all"})
public class HashSet01 {
public static void main(String[] args) {
HashSet hashSet = new HashSet();
//1.在执行add方法后,会返回一个boolean值;如果添加成功则返回一个true,否则就返回一个false
//2.可以通过remove指定删除哪个对象
System.out.println(hashSet.add("john"));//t
System.out.println(hashSet.add("lucy"));//t
System.out.println(hashSet.add("john"));//f
System.out.println(hashSet.add("jack"));//t
System.out.println(hashSet.add("Rose"));//t
hashSet.remove("john");
System.out.println(hashSet);//[Rose, lucy, jack]
}
}
- HashSet底层机制说明
(1). 分析HashSet底层是HashMap,HashMap底层是(数组+链表+红黑树)
(2). 模拟简单的数组+链表结构HashSetStructure.java。先看图在看代码。
package chapter12.set_;
/**
* @aim java基础学习
* @note java笔记
*/
public class HashSetStructure {
public static void main(String[] args) {
//模拟一个HashSet的底层(HashMap的底层结构)
//1.创建一个数组,数组的类型是Node[]
//2.Node[]这个数组也可以成为表
Node[] table=new Node[16];
System.out.println(table);
//3.创建节点
Node john = new Node("john", null);
table[2]=john;
Node jack = new Node("jack", null);
john.next=jack;//将jack挂载到john N
Node rose = new Node("Rose", null);
jack.next=rose;//将rose挂载到jack
Node lucy = new Node("lucy", null);
table[3]=lucy;
System.out.println(table);
}
}
class Node{//节点,存储数据,可以指向下一个节点,从而形成链表
Object item;//存放数据
Node next;//可以指向下一个节点
public Node(Object item, Node next) {
this.item = item;
this.next = next;
}
}
table最终的部分数据如下:其他为null
3. HashSet底层源码分析和结论如下(重点、难点)
(1). 分析HashSet在添加元素时底层是如何实现(hash()+equals())HashSetSource.java
(2). HashSet底层是HashMap
(3). 添加一个元素时,先得到hash值-会转出索引值
(4). 找到存储数据表table,看这个索引位置是否已经存放有元素,如果没有则直接加入;如果有,调用equals比较,如果相同,就放弃添加,如果不相同,则添加到最后。
(5). 在Java8中,如果一条链表的元素个数超过TREEIFY_THRESHOLD(默认是8),并且table的大小>=MIN_TREEIFY_CAPACITY(默认64),就会进化为红黑树。
package chapter12.set_;
import java.util.HashSet;
/**
* @aim java基础学习
* @note java笔记
*/
@SuppressWarnings({"all"})
public class HashSetSource {
public static void main(String[] args) {
HashSet hashSet= new HashSet();
hashSet.add("java");
hashSet.add("python");
hashSet.add("java");
System.out.println(hashSet);
/*
第一步:执行 HashSet hashSet= new HashSet();创建HashSet对象
public HashSet() {
map = new HashMap<>();
}
第二步:执行 hashSet.add("java");
public boolean add(E e) { //e="java"
return map.put(e, PRESENT)==null;
}
其中PRESENT:private static final Object PRESENT = new Object(); 它是不可变的,起一个站位符作用
第三步:执行 put()方法
public V put(K key, V value) { //key="java" value=PRESENT 共享的
return putVal(hash(key), key, value, false, true);
}
其中要进入hash(kay) 方法中不能用 步入 F7 要使用 强制步入 ctrl+shift+F7
执行hash(key)方法 会得到key对应的hash值,算法:h = key.hashCode()) ^ (h >>> 16);
这个算法这样做可以理解:为了不同的key可以得到不同的hash值,从而避免碰撞。且这个hash不等价于hashCode
第四步:执行putVal()方法,对下面代码进行解析
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;//定义了辅助变量
//table就是HashMap 的一个数组,类型就是Node[]
//if语句表示如果当前table是null 或者大小=0
//即进行第一次扩容,resize()就是对table扩容的方法操作,即扩容到16个空间
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//1.从上面得到n=16之后,根据key,得到hash 去计算该key应该存放到table表中的哪个索引位置,
// hash=hash(key),hash(key)是传入的值
//2.并把这个位置的对象 ,赋值给p,即p=tab[i],即指向链表的第一个节点
//3.通过进行&按位与运算,得到具体的索引位置,并判断这个位置是否为空
// 如果为空,就创建一个Node(key="java",value=PRESENT,next=null)
// 如果不为空,就进入else语句:又有三个判断。当再次add("java"),就会else,因为它们计算出来的索引i都一样
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//一个开发技巧提示:在需要局部变量(辅助变量)的时候,再创建
Node<K,V> e; K k;
//如果当前索引位置对应的链表的第一个元素和准备添加的key的hash值一样
//并且满足 下面两个条件之一:
//(1).p.key(p指向链表的第一个节点的key)和准备要添加的key是同一个对象
//(2).通过要添加的key的equals和 p指向链表的第一个节点的key比较后相同
//则不能添加
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//再判断p是不是一颗红黑树,如果是一颗红黑树,就调用putTreeVal()来进行添加
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//如果table对应索引位置,已经是有多个Node链表,就是使用for循序进行比较,从第二个开始比较,因为上面第一个if已经比较了
//(1).依次和该链表的每个元素比较后,都不相同,则添加到该链表的最后
注意:在把该元素添加到链表最后时,立即判断该链表是否已经达到了8个节点,
就调用treeifyBin(),对当前索引的链表进行树化操作(转成红黑树)
注意:在转成红黑树时,还需进行判断(该table是否达到了64),如果达到就转成红黑树,否则就继续扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
//(2).依次和该链表中的每个元素比较过程中,如果有相同情况,就直接break,不添加
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//下面这个if就是判断是否需要进行树化
if (binCount >= TREEIFY_THRESHOLD(8) - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//经过了上面三个条件判断,如果存在相同的情况,就将存在的这个节点赋值给e
//最终return oldValue;即不返回null,就表示添加失败
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)//threshold临界值,如果当前数组的元素大于这个临界值,就执行resize()扩容操作
resize();
afterNodeInsertion(evict);//在HashMap中方法体是空的,其主要目的是让其子类实现
return null;//返回null表示添加元素成功
}
*/
}
}
- HashSet的扩容和转成红黑树机制
(1). HashSet底层是HashMap,第一次添加时,table数组扩容到16,临界值(threshold)是16加载因子(LoadFactor)是0.75=12
(2). 如果table数组使用到临界值12,就会扩容到162=32,新临界值就是32*0.75=24,依次类推。
(3). 在Java8中,如果一条链表的元素个数到达TREEIFY_THRESHOLD(默认是8),并且table的大小>=MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树),否则仍然采用数组扩容机制。
(4). 每添加一个节点Node,就会size+=1,就不管是否在同一个位置上形成链表,即所有的节点Node达到了临界值12就会扩容
package chapter12.set_;
import java.util.HashSet;
import java.util.Objects;
/**
* @aim java基础学习
* @note java笔记
*/
@SuppressWarnings({"all"})
public class HashSetIncrement {
public static void main(String[] args) {
/*
debug查看table数组
HashSet底层是HashMap,第一次添加时,table数组扩容到16,临界值(threshold)是16*加载因子(LoadFactor)是0.75=12
如果table数组使用到临界值12,就会扩容到16*2=32,新临界值就是32*0.75=24,依次类推。
*/
HashSet hashSet = new HashSet();
// for (int i = 0; i < 100; i++) {
// hashSet.add(i);
// }
/*
在Java8中,如果一条链表的元素个数到达TREEIFY_THRESHOLD(默认是8),
并且table的大小>=MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树),否则仍然采用数组扩容机制。
也是使用debug查看
*/
for (int i = 1; i <= 12; i++) {
hashSet.add(new A(i));
}
}//当i=8时,链表就满了,超过8时就会进行一个判断是否需要进行树化,或者需要扩容直到64
// 当i=9,i=10都是进行扩容,链表的位置可能会发生变化:tab[i = (n - 1) & hash]
//当i=11时,就进行树化,链表 由Node变成了TreeNode 红黑树
}
class A{
private int n;
public A(int n) {
this.n = n;
}
//不重写equals(),又保证了它们对比的不是值相等
@Override
public int hashCode() {//重写HashCode(),保证计算出来的hash不一样:h = key.hashCode()) ^ (h >>> 16);
return 100;
}
}
12.3.13 HashSet练习
- 定义一个Employee类,该类包含:private成员属性name,age要求:
HashSetExercise01.java
(1). 创建3个Employee放入HashSet中
(2). 当name和age的值相同时,认为是相同员工,不能添加到HashSet集合中
package chapter12.set_;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Objects;
/**
* @aim java基础学习
* @note java笔记
*/
@SuppressWarnings({"all"})
public class HashSetExercise01 {
public static void main(String[] args) {
Employee jack = new Employee("jack", 18);
Employee mark1 = new Employee("mark", 18);
Employee john = new Employee("john", 18);
Employee mark2 = new Employee("mark", 18);
HashSet hashSet = new HashSet();
hashSet.add(jack);
hashSet.add(mark1);
hashSet.add(mark2);
hashSet.add(john);
Iterator iterator=hashSet.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
}
}
class Employee {
private String name;
private int age;
public Employee(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Employee employee = (Employee) o;
return age == employee.getAge() && name.equals(employee.getName());
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public String toString() {
return "name=" + name + " age=" + age ;
}
}
/*
name=john age=18
name=jack age=18
name=mark age=18
*/
- 定义一个Employee类,该类包含:private成员属性name,sal,birthday(MyDate类型),其中birthday为MyDate类型(属性包括:year,month,day),要求:
(1). 创建3个Employee放入到HashSet
(2). 当name和birthday的值相同时,认为是相同员工,不能添加到HashSet集合中。
package chapter12.set_;
import java.util.HashSet;
import java.util.Objects;
/**
* @aim java基础学习
* @note java笔记
*/
@SuppressWarnings({"all"})
public class HashSetExercise02 {
public static void main(String[] args) {
MyDate myDate1 = new MyDate("2011", "10", "23");
MyDate myDate2 = new MyDate("2000", "3", "12");
HashSet hashSet=new HashSet();
hashSet.add(new EmployeeInfo("mark",3000,myDate1));
hashSet.add(new EmployeeInfo("jack",1600,myDate1));
hashSet.add(new EmployeeInfo("mark",2100,myDate2));
hashSet.add(new EmployeeInfo("mark",2300,myDate1));
for (Object o :hashSet) {
System.out.println(o);
}
/*输出如下:
Employee_{name='jack', sal=1600.0, birthday=2011-10-23}
Employee_{name='mark', sal=2100.0, birthday=2000-3-12}
Employee_{name='mark', sal=3000.0, birthday=2011-10-23}
*/
}
}
@SuppressWarnings({"all"})
class EmployeeInfo{
private String name;
private double sal;
private MyDate birthday;
public EmployeeInfo(String name, double sal, MyDate birthday) {
this.name = name;
this.sal = sal;
this.birthday = birthday;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
EmployeeInfo employee = (EmployeeInfo) o;
return Objects.equals(name, employee.name) && Objects.equals(birthday, employee.birthday);
}
//确保是姓名和出生日期是相同的,其hash值也相同
@Override
public int hashCode() {
return Objects.hash(name,birthday.hashCode());
}
//确保可以看到输出信息
@Override
public String toString() {
return "Employee_{" +
"name='" + name + '\'' +
", sal=" + sal +
", birthday=" + birthday +
'}';
}
}
@SuppressWarnings({"all"})
class MyDate{
private String year;
private String month;
private String day;
public MyDate(String year, String month, String day) {
this.year = year;
this.month = month;
this.day = day;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MyDate myDate = (MyDate) o;
return Objects.equals(year, myDate.year) && Objects.equals(month, myDate.month) && Objects.equals(day, myDate.day);
}
//获取birthday的hash,即出生日期相同的hash就相同
@Override
public int hashCode() {
return Objects.hash(year, month, day);
}
//确保可以看到输出信息
@Override
public String toString() {
return year + "-" + month + "-" + day ;
}
}
12.3.14 Set接口实现类-LinkedHashCodeSet
- LinkedHashSet的全面说明
(1). LinkedHahsSet是HashSet的子类
(2). LinkedHashSet底层是一个LinkedHashSet,底层维护了一个数组+双向链表
(3). LinkedHashSet根据元素的HashCode值来决定元素的存储位置,同时使用链表维护元素的次序(图),这使得元素看起来是以插入顺序来保存的。
(4). LinkedHashSet不允许添加重复元素
2. LinkedHashSet源码分析(debug分析)
package chapter12.set_;
import java.util.*;
/**
* @aim java基础学习
* @note java笔记
*/
@SuppressWarnings({"all"})
public class LinkedHashSetSource {
public static void main(String[] args) {
Set set=new LinkedHashSet();
set.add("hello");
set.add("456");
set.add("456");
set.add(new Customer());
set.add("124");
set.add("hi");
//1.LinkedHashSet 添加顺序和取出元素/数据的顺序一致
//2.LinkedHashSet 底层维护的是一个LinkedHashSet(是HashMap的子类)
//3.LinkedHashSet 底层结构(数组table+双向链表)
//4.添加第一次时,直接将 数组table 扩容到 16 ,存放的节点类型是 LinkedHashMap$Entry
//5.数组是 HashMap$Node[] 存放的元素/数据类型是 LinkedHashMap$Entry
// 6.用的也是putVal()方法,和前面HashSet的一样。
/*
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
*/
System.out.println(set);//[hello, 456, chapter12.set_.Customer@1e643faf, 124, hi]
}
}
class Customer{}
12.3.15 LinkedHashSet练习
- Car类(属性:name,price),如果name和price一样,则认为是相同元素,就不能添加。
package chapter12.set_;
import java.util.LinkedHashSet;
import java.util.Objects;
import java.util.Set;
/**
* @aim java基础学习
* @note java笔记
*/
@SuppressWarnings({"all"})
public class LinkedHashSetExercise01 {
public static void main(String[] args) {
Set set=new LinkedHashSet();
set.add(new Cat("小黑车", 20000));//t
set.add(new Cat("小黑车", 30000));//t
set.add(new Cat("小白车", 10000));//t
set.add(new Cat("小黑车", 20000));//f
set.add(new Cat("小紫车", 20000));//t
set.add(new Cat("小黑车", 20000));//f
System.out.println(set);
//[
//Cat{name='小黑车', price=20000.0},
//Cat{name='小黑车', price=30000.0},
//Cat{name='小白车', price=10000.0},
//Cat{name='小紫车', price=20000.0}]
}
}
class Cat{
private String name;
private double price;
public Cat(String name, double price) {
this.name = name;
this.price = price;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Cat cat = (Cat) o;
return Double.compare(cat.price, price) == 0 && Objects.equals(name, cat.name);
}
@Override
public int hashCode() {
return Objects.hash(name, price);
}
@Override
public String toString() {
return "\nCat{" +
"name='" + name + '\'' +
", price=" + price +
'}';
}
}
12.3.16 Set实现接口类-TreeSet
- TreeSet源码解读
package chapter12.set_;
import java.util.Comparator;
import java.util.TreeSet;
/**
* @aim java基础学习
* @note java笔记
*/
@SuppressWarnings({"all"})
public class TreeSet_ {
public static void main(String[] args) {
//1.当使用TreeSet的无参构造器,数据仍然是无序的
//2.现在希望在添加元素的时候,按照字符串大小来排序
//3.这个时候就可以使用TreeSet提供的一个有参构造器,并可以传入一个比较器(匿名内部类实现) ,并且指定排序规则
// 参照之前的Arrays的练习和String的compareTo()
TreeSet treeSet = new TreeSet(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
return ((String) o1).compareTo((String) o2);
}
});
treeSet.add("jack");
treeSet.add("tom");
treeSet.add("single");
treeSet.add("abc");
System.out.println(treeSet);// [abc, jack, single, tom]
}
/* 源码解析是如何进行排序的(具体讲排序其他的不讲):
1.构造器中传入的比较器对象,其实是赋给了TreeSet的底层TreeMap的属性this.comparator
(1).加载类信息之类的,然后进行构造器初始化
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
(2).实际上使用的TreeMap对象,然后将这个构造器对象赋给TreeMap的属性
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
2.接下来就看看它是如何进行排序的,添加第一个元素就不用看了,默认添加
(1).先执行add()方法
public boolean add(E e) {
return m.put(e, PRESENT)==null;//m.put(e, PRESENT)返回null表示添加成功
}
(2).实际上执行的是TreeMap的put()方法
public V put(K key, V value) {
int cmp;
Entry<K,V> parent;
//这个comparator,就是初始化赋的值,也就是那个匿名内部类对象(实现Comparator接口),现在又赋给cpr
Comparator<? super K> cpr = comparator;
if (cpr != null) {
do {
parent = t;
//compare()就是重写的方法,key是当前要添加的key,t.key是已经添加成功的元素,这里做循环去比较
//调用重写的compare(),若返回负数,则不移;若返回正数,key则往前移;若相同不添加
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
}
*/
}
- TreeSet练习
要求使用TreeSet添加元素的时候,按照字符串的长度来排序。
package chapter12.set_;
import java.util.Comparator;
import java.util.TreeSet;
/**
* @aim java基础学习
* @note java笔记
*/
@SuppressWarnings({"all"})
public class TreeSetExercise {
public static void main(String[] args) {
TreeSet treeSet = new TreeSet(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
return ((String) o1).length() - ((String) o2).length();
}
});
treeSet.add("abcdef");
treeSet.add("abcde");
treeSet.add("abcd");
treeSet.add("abc");
treeSet.add("tom");//问这个tom可以添加吗?答:不可以,因为底层己经改为判断了同一长度不添加
System.out.println(treeSet);//[abc, abcd, abcde, abcdef]
}
}
12.4 Map
12.4.1 Map接口实现类的特点
- 注意:这里讲的是JDK8的Map接口特点
(1). Map与Collection并列存在。用于保存具有映射关系的数据Key-Value
(2). Map中的Key和Value可以是任何引用类型的数据,会封装到HashMap$Node对象中
(3). Map中的key不允许重复,原因和HashSet一样,前面分析过源码
(4). Map中的value可以重复。
(5). Map中的key可以为null,value也可以为null,注意可以为null只能有一个,而value为null可以有多个。
(6). 最常用String类作为Map的key
(7). key和value之间存在单向一对一关系,即通过指定的key总能找到对应的value
package chapter12.map_;
import java.util.HashMap;
import java.util.Map;
/**
* @aim java基础学习
* @note java笔记
*/
@SuppressWarnings({"all"})
public class Map_ {
public static void main(String[] args) {
//1.Map与Collection并列存在。用于保存具有映射关系的数据Key-Value
//2.Map中的Key和Value可以是任何引用类型的数据,会封装到HashMap$Node对象中
//3.Map中的key不允许重复,原因和HashSet一样,前面分析过源码
//4.Map中的key可以为null,value也可以为null,注意可以为null只能有一个,而value为null可以有多个。
//5.最常用String类作为Map的key
//6.key和value之间存在单向一对一关系,即通过指定的key总能找到对应的value
Map map=new HashMap();
map.put("no.1","小明");//第一个值是k,第二个是v
map.put("no.2","小黄");
map.put("no.1","小菊");//当有相同的k时,就等价于把v替换了
map.put("no.3","小菊");
map.put(null,null);
map.put(null,"abc");//又相当于替换k,所以只能有一个null
map.put("np.4",null);//但是相同的v可以有多个,只是k不一样。
map.put(1,"笑笑");
System.out.println(map); //{null=abc, 1=笑笑, no.3=小菊, no.2=小黄, no.1=小菊, np.4=null}
//通过get方法, 传入 key , 会返回对应的value
System.out.println(map.get("no.2"));//小黄
}
}
(8). Map存放数据的key-value示意图,一对k-v是放在一个HashMap$Node中的,有因为Node实现了Entry接口,有些书也说一对k-v就是一个Entry
package chapter12.map_;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* @aim java基础学习
* @note java笔记
*/
@SuppressWarnings({"all"})
public class MapSource_ {
public static void main(String[] args) {
Map map=new HashMap();
map.put("no.1","小明");//第一个值是k,第二个是v
map.put("no.2","小黄");
//1. k-v值最后是存储在 tab[i] = newNode(hash, key, value, null);由之前代码知道tab[i]实际上是HashMap$Node类型的
// 所以实际上是HashMap$Node node = newNode(hash, key, value, null);
//2. k-v 为了方便程序员的遍历, 还会 创建EntrySet集合 ,该集合存放的元素类型是 Entry, 而一个Entry对象就有k,v,
// 也就是EntrySet<Entry<K,V>>,即 transient Set<Map.Entry<K,V>> entrySet;
//3. 在entrySet中, 定义的类型是 Map.Entry ,但实际上存放的类型还是 HashMap$Node,其实也就是一个向上转型,如下源码:
// static class Node<K,V> implements Map.Entry<K,V>
//4. 当把 HashMap$Node 对象 存放到entrySet 就方便了遍历它的k-v,因为在Map.Entry 中提供了相关的方法,如下源码
// interface Entry<K, V> {K getKey();V getValue();.....}
Set set=map.entrySet();
System.out.println(set.getClass());//HashMap$EntrySet
for (Object entry :set) {
System.out.println(entry.getClass());//HashMap$Node
}
for (Object o :set) {
Map.Entry entry=(Map.Entry) o;
System.out.println(entry.getKey()+"-"+entry.getValue());
//no.2-小黄
//no.1-小明
}
Set key=map.keySet();
System.out.println(key.getClass());//HashMap$KeySet
Collection value=map.values();
System.out.println(value.getClass());//HashMap$Values
}
}
12.4.2 Map接口常用方法
- put:添加键值对。如果键已经存在,则更新对应的值。
- remove:根据键删除映射关系,如果键不存在,则不做任何操作。
- get:根据键获取值。如果键不存在,返回 null。
- size:返回 Map 中键值对的数量。
- isEmpty:检查 Map 是否为空,即是否没有键值对
- clear:清空 Map 中的所有键值对
- containsKey:查找键是否存在
package chapter12.map_;
import jdk.nashorn.api.tree.GotoTree;
import java.util.HashMap;
import java.util.Map;
/**
* @aim java基础学习
* @note java笔记
*/
@SuppressWarnings({"all"})
public class MapMethod {
public static void main(String[] args) {
//1. put:添加键值对。如果键已经存在,则更新对应的值。
//2. remove:根据键删除映射关系,如果键不存在,则不做任何操作。
//3. get:根据键获取值。如果键不存在,返回 null。
//4. size:返回 Map 中键值对的数量。
//5. isEmpty:检查 Map 是否为空,即是否没有键值对
//6. clear:清空 Map 中的所有键值对
//7. containsKey:查找键是否存在
Map map = new HashMap();
map.put("1", "苹果"); // 添加键值对
map.put("2", "香蕉");
map.put("1",new Fruit("草莓",100));
map.put("3", "香蕉");
map.put("4", null);
map.put(null,"石榴");
System.out.println(map);//{null=石榴, 1=name=草莓和price=100.0, 2=香蕉, 3=香蕉, 4=null}
map.remove("1");
System.out.println(map);//{null=石榴, 2=香蕉, 3=香蕉, 4=null}
System.out.println(map.get(null));//石榴
boolean containsKey= map.containsKey("3");
System.out.println(containsKey);//true
System.out.println(map.containsValue("香蕉"));//true
System.out.println(map.size());//4
System.out.println(map.isEmpty());//false
map.clear();
System.out.println(map);//{}
}
}
class Fruit{
private String name;
private double price;
public Fruit(String name, double price) {
this.name = name;
this.price = price;
}
@Override
public String toString() {
return "name=" + name + "和price=" + price ;
}
}
12.4.3 Map接口遍历的方法
- containsKey:查找键是否存在
- keySet:获取所有键
- entrySet:获取所有关系
- values:获取所有的值
package chapter12.map_;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
/**
* @aim java基础学习
* @note java笔记
*/
@SuppressWarnings({"all"})
public class MapFor {
public static void main(String[] args) {
Map map = new HashMap();
map.put("1", "苹果"); // 添加键值对
map.put("2", "香蕉");
map.put("3", "香蕉");
map.put("4", "西瓜");
map.put("5","石榴");
Set keys=map.keySet();
System.out.println("======第一种方式=======");
Iterator iterator=keys.iterator();
while(iterator.hasNext()){
Object key=iterator.next();
Object value=map.get(key);
System.out.println(key+"-"+value);
}
System.out.println("=====第二种方式=======");
for (Object key :keys) {
System.out.println(key+"-"+map.get(key));
}
System.out.println("====第三种方式======");
Set entrys=map.entrySet();
for (Object entry :entrys) {
Map.Entry e=(Map.Entry) entry;
Object key=e.getKey();
Object value=e.getValue();
System.out.println(key+"-"+value);
}//也可以用迭代器
}
}
/*
======第一种方式=======
1-苹果
2-香蕉
3-香蕉
4-西瓜
5-石榴
=====第二种方式=======
1-苹果
2-香蕉
3-香蕉
4-西瓜
5-石榴
====第三种方式======
1-苹果
2-香蕉
3-香蕉
4-西瓜
5-石榴
*/
12.4.4 Map接口练习
- 使用HashMap添加3个员工对象,要求键:员工id;值:员工对象并遍历显示工资>18000的员工(遍历方式最小两种)员工类:姓名、工资、员工id
package chapter12.map_;
import java.util.*;
/**
* @aim java基础学习
* @note java笔记
*/
@SuppressWarnings({"all"})
public class MapExercise01 {
public static void main(String[] args) {
Employee mark = new Employee("mark", "213", 15000);
Employee jack = new Employee("jack", "214", 20000);
Employee rose = new Employee("rose", "215", 26000);
Map map = new HashMap();
map.put(mark.getId(), mark);
map.put(jack.getId(), jack);
map.put(rose.getId(), rose);
System.out.println("=====增强for遍历方式======");
Set keys = map.keySet();
for (Object key : keys) {
Employee employee=(Employee) map.get(key);
if(employee.getSalary()>18000){
System.out.println(key + "-" + map.get(key));
}
}
System.out.println("=====迭代器遍历方式======");
Set entrys = map.entrySet();
Iterator iterator = entrys.iterator();
while (iterator.hasNext()){
Map.Entry es=(Map.Entry) iterator.next();
Employee employee=(Employee) es.getValue();
if(employee.getSalary()>18000){
System.out.println(es.getKey() + "-" + es.getValue());
}
}
/*
=====增强for遍历方式======
214-Employee{name='jack', salary=20000.0}
215-Employee{name='rose', salary=26000.0}
=====迭代器遍历方式======
214-Employee{name='jack', salary=20000.0}
215-Employee{name='rose', salary=26000.0}
*/
}
}
@SuppressWarnings({"all"})
class Employee {
private String name;
private String id;
private double salary;
public Employee(String name, String id, double salary) {
this.name = name;
this.id = id;
this.salary = salary;
}
public String getId() {
return id;
}
public double getSalary() {
return salary;
}
@Override
public String toString() {
return "Employee{" +
"name='" + name + '\'' +
", salary=" + salary +
'}';
}
}
12.4.5 Map实现接口类-HashMap
-
HashMap小结
(1). Map接口的常用实现类:HashMap、Hashtable和Properties
(2). HashMap是Map接口使用频率最高的实现类。
(3). HashMap是以key-val对的方式来存储数据(HashMap$Node类型)
(4). Key不能重复,但是值是可以重复的,允许使用null键和null值
(5). 如果添加相同的key,则会覆盖原来的key-val,等同与修改(key不会替换,val会替换)
(6). 与HashSet一样,不保证映射的顺序,因为底层是以hash表的方式来存储的(JDK8的HashMap 底层是 数组+链表+红黑树)
(7). HashMap没有实现同步,因此是线程不安全的,方法没有做同步互斥的操作,没有synchronized -
HashMap底层机制及源码剖析
看一个示意图
(1). (k,v)是一个Node节点 实现了Map.Entry<K,V>,查看HashMap的源码时可以看到
(2). Jdk7.0的HashMap底层实现【数组+链表】,jdk8.0 底层【数组+链表+红黑树】 -
HashMap底层扩容机制【和HashSet相同】
(1). HashMap底层维护了Node类型的数组table,默认为null
(2). 当创建对象时,将加载因子(loadFactor)初始化为0.75.
(3). 当添加key-val时,通过key的哈希值得到在table的索引。然后判断该索引是否有元素,如果没有元素直接添加。如果该索引有元素,继续判断该元素的key是否和准备加入的key相等,如果相等,则直接替换val;如果不相等需要判断是树结构还是链表结构,做出相应的处理。如果添加时发现容量不够,则需要扩容。
(4). 第1次添加,则需要扩容table容量为16,临界值(threshold)为12(160.75)
(5). 以后再扩容,则需要扩容table容量为原来的2倍,临界值为原来的两倍即24(320.75),依次类推。
(6). 在Java8中,如果一条链表的元素个数超过TREEIFY_THRESHOLD(默认是8),并且table的大小 >= MIN_TREEIF_CAPACITY(默认64),就会进化树化(红黑树) -
HashMap底层源码剖析
package chapter12.map_;
import java.util.HashMap;
/**
* @aim java基础学习
* @note java笔记
*/
@SuppressWarnings({"all"})
public class HashMapSource {
public static void main(String[] args) {
HashMap hashMap = new HashMap();
hashMap.put("java", 10);
hashMap.put("python", 10);
hashMap.put("java", 20);
System.out.println(hashMap);
/*
1.创建HashMap对象时,初始化加载因子 loadFactor=0.75,HashMap$Node[] table=null;
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
2.执行put("java"),先装箱,然后在执行put,其中key="java" value=10
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
其中hash(key)方法是计算hash值返回,代码如下: 将原始哈希码h 按位异或^ 无符号右移操作符 >>> 将 h 右移 16 位
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
3.得到hash值之后,就开始执行putVal()方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;//辅助变量
//如果底层的table 数组为null 或者 length=0 ,就扩容到16
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//tab[i = (n - 1) & hash]&按位与运算,计算出索引的位置,如果该索引位置为null
//就直接创建一个Node,将k-v加入到该Node,最后放入到该索引位置即可。
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;//辅助变量
//如果当前索引位置对应的链表的元素和准备添加的key的hash值一样
//并且满足 下面两个条件之一:
//(1).p.key(p指向链表的第一个节点的key)和准备要添加的key是同一个对象
//(2).通过要添加的key的equals和 p指向链表的第一个节点的key比较后相同
//则不能添加 将该位置的Node赋给e
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//判断p是不是一颗红黑树,如果是一颗红黑树,就调用putTreeVal()来进行添加
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//如果找到节点,后面是链表,就用循环去比较
for (int binCount = 0; ; ++binCount) {//死循环
//如果整个链表Node的可以没有和要添加的key相同,就添加到该链表的最后
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//添加后,判断当前链表个个数,如果已经到8个了,就调用treeifyBin方法进行红黑树转换
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//这里e不是空的,执行上面的if e = p.next后得到了节点,如果在循环比较过程中,发现有相同 就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;//替换key对应的value值
afterNodeAccess(e);
return oldValue;
}
}
++modCount;//每增加一个Node,就size++
if (++size > threshold)//如size > 临界值,就扩容
resize();
afterNodeInsertion(evict);
return null;
}
4.关于树化(转成红黑树)
// 如果table 为null,或者大小还没到64 ,暂时不树化,而是进行扩容
// 否则才会真正的树化 -> 剪枝
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
*/
}
}
- 模拟HashMap触发扩容、树化情况,并验证。Debug
(1). 将同一个索引的地方一直挂载元素,看看什么时候进行扩容,什么时候进行树化。
package chapter12.map_;
import java.util.HashMap;
import java.util.Objects;
/**
* @aim java基础学习
* @note java笔记
*/
@SuppressWarnings({"all"})
public class HashMapIncrement {
public static void main(String[] args) {
HashMap hashMap = new HashMap();
for (int i = 1; i <= 12; i++) {
hashMap.put(new A(i),10+i);
}
System.out.println(hashMap);
}
//当i=1时,进行第一次扩容 table=16
//当i=8时,链表就满了,超过8时就会进行一个判断是否需要进行树化,或者需要扩容直到64
//当i=9,i=10都是进行扩容,链表的位置可能会发生变化:tab[i = (n - 1) & hash], n = tab.length, tab=table
//当i=11时,就进行树化,链表 由Node变成了TreeNode 红黑树
}
class A{
private int num;
public A(int num) {
this.num = num;
}
//所有A对象的HashCode都是100,保证了所有的A对象在同一索引位置上,这样可以清楚看到扩容效果和树化效果
@Override
public int hashCode() {
return 100;
}
@Override
public String toString() {
return "A{" +
"num=" + num +
'}';
}
}
(2). 在table每个索引中都添加元素,查看临界值到达哪里会扩容,扩容多少。
package chapter12.map_;
import java.util.HashMap;
/**
* @aim java基础学习
* @note java笔记
*/
@SuppressWarnings({"all"})
public class HashMapInrement02 {
public static void main(String[] args) {
HashMap hashMap = new HashMap();
for (int i = 1; i <= 13; i++) {
hashMap.put(i, "hello");
}
for (int i = 14; i <= 25; i++) {
hashMap.put(i, "hello");
}
for (int i = 26; i <= 49; i++) {
hashMap.put(i, "hello");
}
for (int i = 50; i <= 97; i++) {
hashMap.put(i, "hello");
}
for (int i = 98; i <= 1073741824; i++) {
hashMap.put(i, "hello");
}
hashMap.put(null,2);
hashMap.put("1",2);
//0->16(12)->32(24)->64(48)->128(96).....
//正常若Node节点超过了临界值就会进行扩容,但是这个数组也是有最大值的,达到最大值1073741824后,就不会再次扩容了
//理论是这样的,但是数太大了,电脑不好的不建议验证
}
}
12.4.6 Map实现接口类-Hashtable
-
Hashtable的基本介绍
(1). 存放的元素是键值对:即k-v
(2). Hashtable的键和值都不能为null
(3). Hashtable使用方法基本上和HashMap一样
(4). Hashtable是线程安全的,HashMap是线程不安全的
(5). 了解底层结构 -
判断下面的代码是否正确,如果错误,为什么
package chapter12.map_;
import java.util.Hashtable;
/**
* @aim java基础学习
* @note java笔记
*/
@SuppressWarnings({"all"})
public class HashTableExercise01 {
public static void main(String[] args) {
Hashtable hashtable=new Hashtable();
hashtable.put("jack",100);//ok
// hashtable.put(null,100);//NullPointerException异常,键不能为null
// hashtable.put("jack",null);//NullPointerException异常,值不能为null
hashtable.put("lucy",100);//ok
hashtable.put("rose",100);//ok
hashtable.put("rose",500);//替换
for (int i = 1; i <= 6; i++) {
hashtable.put(i,10);
}
System.out.println(hashtable);//{jack=100, lucy=100, 6=10, 5=10, 4=10, 3=10, 2=10, 1=10, rose=500}
//Hashtable的底层:
//1.底层有一个数组 Hashtable$Entry[] 初始化大小为11
//2.临界值 threshold 为8, 11*0.75=8
//3.Hashtable 扩容机制:直接从添加第九个元素开始debug,
/*
(1).先对基本数据类型进行装箱操作
(2).执行put()方法
(3).然后执行真正添加到数据的方法:addEntry(hash, key, value, index); 添加k-v 封装到Entry
(4).再然后执行addEntry方法的判断语句:count表示hashtable当前存在元素的个数,threshold=8临界值
if (count >= threshold) {
// Rehash the table if the threshold is exceeded
rehash();
tab = table;
hash = key.hashCode();
index = (hash & 0x7FFFFFFF) % tab.length;
}
明显上面的if条件为true,进行扩容操作rehash()
(5). rehash()里面的方法就按照 int newCapacity = (oldCapacity << 1) + 1;规则进行扩容,也就是乘2加+1,具体的怎么实现,自行查看
*/
}
}
3. Hashtable和HashMap对比
发行版本 | 线程安全(同步) | 效率 | 允许null键null值 | |
---|---|---|---|---|
HashMap | 1.2 | 不安全 | 高 | 可以 |
Hashtable | 1.0 | 安全 | 较低 | 不可以 |
12.4.7 Map实现接口类-Properties
- 基本介绍
(1). Properties类继承自Hashtable类并实现了Map接口,也是使用一种键值对的形式来保存数据。
(2). 它的使用特点和Hashtable类似
(3). Properties还可以用于从xxx.properties文件中,加载数据到Properties类对象,并进行读取和修改
(4). 说明:工作后 xxx.properties文件通常作为配置文件,这个知识点在IO流举例。
package chapter12.map_;
import java.util.Properties;
/**
* @aim java基础学习
* @note java笔记
*/
@SuppressWarnings({"all"})
public class Properties_ {
public static void main(String[] args) {
Properties properties = new Properties();
properties.put("john", 100);
// properties.put(null, 100);//NullPointerException异常,键不能为null
// properties.put("jack", null);//NullPointerException异常,值不能为null
properties.put("lucy", 100);//ok
properties.put("rose", 100);//ok
properties.put("rose", 500);//替换
System.out.println(properties);//{john=100, rose=500, lucy=100}
//查找,如果使用的getProperty(key),它会判断值是不是String,如果不是就返回null
//至于getProperty(key,defaultValue),它会判断getProperty(key)是不是null,如果是就返回defaultValue
System.out.println(properties.get("john"));//100
System.out.println(properties.getProperty("john"));//null
System.out.println(properties.getProperty("john","没有该键"));//没有该键
//删除
properties.remove("rose");
System.out.println(properties);//{john=100, lucy=100}
//修改
properties.put("lucy","hello");
System.out.println(properties);//{john=100, lucy=hello}
}
}
12.4.8 Map实现接口类-TreeMap
- TreeMap源码解析:
package chapter12.map_;
import java.util.Comparator;
import java.util.TreeMap;
/**
* @aim java基础学习
* @note java笔记
*/
@SuppressWarnings({"all"})
public class TreeMap_ {
public static void main(String[] args) {
/*
1.使用默认的无参构造器,创建TreeMap,是无序的,即没有排序
2.现要求按照传入的key(String)的大小进行排序,从z-a
3.put(e, PRESENT),其中e是key PRESENT是值,即
private static final Object PRESENT = new Object();
*/
TreeMap treeMap = new TreeMap(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
return ((String) o2).compareTo(((String) o1));
}
});
treeMap.put("Mark", "hello");
treeMap.put("Jack", "hello");
treeMap.put("Alan", "hello");
treeMap.put("John", "hello");
treeMap.put("Bella", "hello");
System.out.println(treeMap);
//{Mark=hello, John=hello, Jack=hello, Bella=hello, Alan=hello}
/*
1.构造器,把传入的实现了Comparator接口的匿名内部类(对象),传给给TreeMap的comparator
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
2.调用put方法
2.1第一次添加, 把k-v 封装到Entry对象, 放入到root中
Entry<K,V> t = root;
if (t == null) {//第一次添加时,其实也调用了匿名内部类的compare,其目的是防治传入的值是null,方便抛出异常。
compare(key, key); // type (and possibly null) check
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
2.2 以后添加的元素
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths
Comparator<? super K> cpr = comparator;
if (cpr != null) {
do { //遍历所有的key,给当前的key找到适当的位置
parent = t;
cmp = cpr.compare(key, t.key);//动态绑定到匿名内部类的compare
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else //如果过程中,发现准备添加的key和当前已有的key相等,那么就不添加
return t.setValue(value);
} while (t != null);
}
*/
}
}
12.5 总结-开发中如何选择集合实现类(记住)
- 在开发中,选择什么集合实现类,主要取决于业务操作特点,然后根据集合实现类特性进行选择,分析如下:
(1). 先判断存储类型(一组对象[单列]或一组键值对[双列])
(2). 一组对象[单列]:Collection接口
① 允许重复:List
- 增删多:LinkedList(底层维护了一个双向链表)
- 改查多:ArrayList(底层维护了Object类型的可变数组)
② 不允许重复[双列]:Set
- 无序:HashSet【底层是HashMap,维护了一个哈希表 即(数组+链表+红黑树)】
- 排序:TreeSet【底层是TreeMap】
- 插入和取出顺序一致:LinkedHashSet【底层是LinkedHashMap,LinkedHashMap的底层HashMap】,维护了数组+双向链表
(3). 一组键值对:Map
① 键无序:HashMap【底层是:哈希表,jdk7:数组+链表 ,jdk8:数组+链表+红黑树】
② 键排序:TreeMap
③ 键插入和取出顺序一致:LinkedHashMap
④ 读取文件:Properties
12.6 Collections工具类
- Collections工具类介绍
(1). Collections是一个操作Set、List和Map等集合的工具类
(2). Collections中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作 - 排序操作
(1). reverse(List): 反转List中元素的顺序
(2). shuffle(List): 对List集合元素进行随机排序
(3). sort(List): 根据元素的自然顺序对指定List集合元素按升序排序
(4). sort(List,Comparetor): 根据指定的Comparator产生的顺序对List集合元素进行排序
(5). swap(List,int,int): 将指定List集合中的i处元素和j处元素进行交换
package chapter12.collections_;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* @aim java基础学习
* @note java笔记
*/
@SuppressWarnings({"all"})
public class Collections_ {
public static void main(String[] args) {
// Collections
//创建ArrayList集合,用于测试
List list = new ArrayList();
list.add("Apple");
list.add("Banana");
list.add("Orange");
list.add("Peach");
System.out.println("原版=" + list);//原版=[Apple, Banana, Orange, Peach]
//(1). reverse(List): 反转List中元素的顺序
Collections.reverse(list);
System.out.println("反转=" + list);//反转=[Peach, Orange, Banana, Apple]
//(2). shuffle(List): 对List集合元素进行随机排序
Collections.shuffle(list);
System.out.println("随机=" + list);//随机=[Orange, Apple, Banana, Peach]
//(3). sort(List): 根据元素的自然顺序对指定List集合元素按升序排序
Collections.sort(list);
System.out.println("自然排序=" + list);//自然排序=[Apple, Banana, Orange, Peach]
//(4). sort(List,Comparetor): 根据指定的Comparator产生的顺序对List集合元素进行排序
Collections.sort(list, new Comparator() {
@Override
public int compare(Object o1, Object o2) {
if(!(o1 instanceof String) || !(o2 instanceof String)){
throw new ClassCastException("类型错误 要String");
}
return ((String) o1).length() - ((String) o2).length();
}
});
System.out.println("比较器排序=" + list);//比较器排序=[Apple, Peach, Banana, Orange]
//(5). swap(List,int,int): 将指定List集合中的i处元素和j处元素进行交换
Collections.swap(list,0,2);
System.out.println("交换=" + list);//交换=[Banana, Peach, Apple, Orange]
}
}
- 查找、替换
(1). Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
(2). Object max(Collection, Comparator):根据Comparator指定的顺序,返回给定集合中的最大元素
(3). Object min(Collection):
(4). Object min(Collection, Comparator):
(5). int frequency(Collection, Object):返回指定集合中指定元素的出现次数
(6). void copy(List dest,List src):将src中的内容复制到dest中
(7). boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换List对象的所有旧值。
package chapter12.collections_;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* @aim java基础学习
* @note java笔记
*/
@SuppressWarnings({"all"})
public class CollectionsMethod {
public static void main(String[] args) {
//创建ArrayList集合,用于测试
List list = new ArrayList();
list.add("Pineapple");
list.add("Cherry");
list.add("Watermelon");
list.add("Stawberry");
System.out.println("原版的List="+list);//原版的List=[Pineapple, Cherry, Watermelon, Stawberry]
//(1). Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
String str= (String) Collections.max(list);
System.out.println(str);//Watermelon
//(2). Object max(Collection, Comparator):根据Comparator指定的顺序,返回给定集合中的最大元素
String str1= (String) Collections.max(list, new Comparator() {
@Override
public int compare(Object o1, Object o2) {
return ((String) o1).length()-((String) o2).length();
}
});//按照长度
System.out.println(str1);//Watermelon
//(3). Object min(Collection):
//(4). Object min(Collection, Comparator):
//(5). int frequency(Collection, Object):返回指定集合中指定元素的出现次数
int f=Collections.frequency(list,"Watermelon");
System.out.println(f);//1
//(6). void copy(List dest,List src):将src中的内容复制到dest中,要求dest的size长度大于等于src,否则抛出异常
List dest = new ArrayList();
for (int i = 0; i < 4; i++) {
dest.add("");
}
Collections.copy(dest,list);
System.out.println("赋值的List="+dest);//赋值的List=[Pineapple, Cherry, Watermelon, Stawberry]
//(7). boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换List对象的所有旧值。
boolean ans=Collections.replaceAll(list,"Watermelon","Apple");
System.out.println("替换后的List="+list);//替换后的List=[Pineapple, Cherry, Apple, Stawberry]
}
}
12.7 本章作业
- 编程题 Task01.java
按要求实现:
(1). 封装一个新闻类,包含标题和内容属性,提供getter和setter方法,重写toString方法,打印对象时只打印标题;
(2). 只提供一个带参数的构造器,实例化对象时,只初始化标题;并且实例化两个对象:
新闻一:新冠确诊病例超千万,数百万印度教信徒赴恒河“圣浴”引民众担忧
新闻二:男字突然想起2个月前钓的鱼还在网兜里,捞起一看赶紧放生
(3). 将新闻对象添加到ArrayList集合中,并且进行倒序遍历;
(4). 在遍历集合过程中,对新闻标题进行处理,超过15字的只保留15个,然后在后边加“…”
(5). 在控制台打印遍历出经过处理的新闻标题
package chapter12.task_;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
/**
* @aim java基础学习
* @note java笔记
*/
@SuppressWarnings({"all"})
public class Task01 {
public static void main(String[] args) {
ArrayList arrayList =new ArrayList();
arrayList.add(new News("新冠确诊病例超千万,数百万印度教信徒赴恒河“圣浴”引民众担忧"));
arrayList.add(new News("男字突然想起2个月前钓的鱼还在网兜里,捞起一看赶紧放生"));
//这里有多种处理,可以在倒序循环中。直接处理字符串,也可以写在方法里。自由发挥
Collections.reverse(arrayList);
Iterator iterator=arrayList.iterator();
while (iterator.hasNext()){
News news=(News) iterator.next();
String title=news.toString();
if(title.length()>15){
System.out.println(title.substring(0,15)+"...");
}else{
System.out.println(title);
}
}
}
}
class News{
private String title;
private String content;
public News(String title) {
this.title = title;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
@Override
public String toString() {
return title;
}
}
- 编程题 Task02.java
使用ArrayList完成对 对象Car{name,price}的各种操作
(1). add:添加单个元素
(2). remove:删除指定元素
(3). contains:查找元素是否存在
(4). size:获取元素个数
(5). isEmpty:判断是否为空
(6). clear:清空
(7). addAll:添加多个元素
(8). containsAll:查找多个元素是否存在
(9). removeAll:删除多个元素
(10). 使用增强for和迭代器来遍历所有的car,需要重写car的toString。
package chapter12.task_;
import java.util.ArrayList;
import java.util.Iterator;
/**
* @aim java基础学习
* @note java笔记
*/
@SuppressWarnings({"all"})
public class Task02 {
public static void main(String[] args) {
Car car1 = new Car("白纸", 20000);
Car car2 = new Car("黑书",70000);
Car car3 = new Car("he",90000);
ArrayList arrayList = new ArrayList();
//(1). add:添加单个元素
arrayList.add(car1);
arrayList.add(car2);
arrayList.add(car3);
System.out.println(arrayList);
//[Car{name='白纸', price=20000.0}, Car{name='黑书', price=70000.0}, Car{name='he', price=90000.0}]
//(2). remove:删除指定元素
arrayList.remove(2);
System.out.println(arrayList);//[Car{name='白纸', price=20000.0}, Car{name='黑书', price=70000.0}]
//(3). contains:查找元素是否存在
boolean c=arrayList.contains(car1);
System.out.println(c);//true
//(4). size:获取元素个数
System.out.println(arrayList.size());//2
//(5). isEmpty:判断是否为空
System.out.println(arrayList.isEmpty());//false
//(6). clear:清空
arrayList.clear();
System.out.println(arrayList);//[]
//(7). addAll:添加多个元素
ArrayList list = new ArrayList();
list.add(car3);
list.add(car2);
arrayList.addAll(list);
System.out.println(arrayList);//[Car{name='he', price=90000.0}, Car{name='黑书', price=70000.0}]
//(8). containsAll:查找多个元素是否存在
System.out.println(arrayList.containsAll(list));//true
//(9). removeAll:删除多个元素
arrayList.removeAll(list);
System.out.println(arrayList);//[]
//(10). 使用增强for和迭代器来遍历所有的car,需要重写car的toString。
System.out.println(list);//[Car{name='he', price=90000.0}, Car{name='黑书', price=70000.0}]
System.out.println("=====增强for=====");
for (Object o :list) {
System.out.println(o);
//Car{name='he', price=90000.0}
//Car{name='黑书', price=70000.0}
}
System.out.println("======迭代器=====");
Iterator iterator=list.iterator();
while (iterator.hasNext()) {
Object next = iterator.next();
System.out.println(next);
//Car{name='he', price=90000.0}
//Car{name='黑书', price=70000.0}
}
}
}
class Car{
private String name;
private double price;
public Car(String name, double price) {
this.name = name;
this.price = price;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
@Override
public String toString() {
return "Car{" +
"name='" + name + '\'' +
", price=" + price +
'}';
}
}
- 编程题 Task03.java
按要求完成下列任务
(1). 使用HashMap类实例化一个Map类型的对象m,键(String)和值(int)分别用于存储员工的姓名和工资,存入数据如下:Jack-650元;Tom-1200元;Rose-2900元;
(2). 将Jack的工资更改为2600元
(3). 为所有员工工资加薪100元
(4). 遍历集合中所有工资
package chapter12.task_;
import java.util.*;
/**
* @aim java基础学习
* @note java笔记
*/
@SuppressWarnings({"all"})
public class Task03 {
public static void main(String[] args) {
Map Map = new HashMap();
//添加数据
Map.put("Jack",650);
Map.put("Tom",1200);
Map.put("Rose",2900);
System.out.println(Map);//{Tom=1200, Rose=2900, Jack=650}
//修改Jack的工资为2600
Map.put("Jack",2600);
System.out.println(Map);//{Tom=1200, Rose=2900, Jack=2600}
//为所有员工加薪100元
Set keys=Map.keySet();
for (Object key :keys) {
int value=(Integer) Map.get(key)+100;
Map.put(key,value);
}
System.out.println(Map);//{Tom=1300, Rose=3000, Jack=2700}
//遍历输出所有工资
Collection values=Map.values();
for (Object value :values) {
System.out.println(value);
//1300
//3000
//2700
}
}
}
-
简答题
分析HashSet和TreeSet分别是如何实现去重的
(1). HashSet的去重机制:HashCode()+equals(),底层先通过存入对象,进行运算得到一个hash值,通过hash值得到对应的索引,如果发现table索引所在的位置,没有数据,就直接存放,如果有数据,就进行equals比较【遍历比较】,如果比较后,不相同,就添加,否则不添加。
(2). TreeSet的去重机制:如果传入了一个Comparator匿名对象,就使用实现的compare去重,如果方法返回0,就认为是相同的元素/数据,就不添加;如果没有传入一个Comparator匿名对象,则以添加对象实现的Compareable接口的compareTo去重。 -
代码分析题
下面代码运行时会不会抛出异常,并从源码层面说明原因。【考察 接口编程+动态绑定】
TreeSet treeSet =new TreeSet();
treeSet.add(new Person());
class Person{}
在add方法中,因为TreeSet()构造器没有传入Comparator接口的匿名内部类;所以底层还是Comparable<? super K> k = (Comparable<? super K>) key; 在这里进行向上转型时,就会抛出ClassCastException异常。解决办法,让Person类实现Comparator接口即可。
- 下面代码输出什么?
已知Person类按照id和name重写了HashCode和equals方法。
package chapter12.task_;
import java.util.HashSet;
import java.util.Objects;
/**
* @aim java基础学习
* @note java笔记
*/
@SuppressWarnings({"all"})
public class Task06 {
public static void main(String[] args) {
HashSet hashSet = new HashSet();
Person a = new Person(1001, "A");
Person b = new Person(1002, "B");
hashSet.add(a);
hashSet.add(b);
a.name = "C";//a的name改变了
System.out.println(hashSet);
hashSet.remove(a);//删除不成功
System.out.println(hashSet);
hashSet.add(new Person(1001, "C"));//添加成功
System.out.println(hashSet);
hashSet.add(new Person(1001, "A"));//添加成功
System.out.println(hashSet);
//[Person{id=1001, name='C'}, Person{id=1002, name='B'}]
//[Person{id=1001, name='C'}, Person{id=1002, name='B'}]
//[Person{id=1001, name='C'}, Person{id=1002, name='B'}, Person{id=1001, name='C'}]
//[Person{id=1001, name='C'}, Person{id=1002, name='B'}, Person{id=1001, name='A'}, Person{id=1001, name='C'}]
}
}
class Person {
int id;
String name;
public Person(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return id == person.id && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(id, name);
}
@Override
public String toString() {
return "Person{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}