Java集合详解
前言
集合的理解和好处
- 前面我们保存多个数据使用的是数组,数组有一些不足的地方,比如:
- 数组的不足之处
- 长度开始时必须指定,而且一旦指定,不能更改
- 保存的必须为同一类型的元素
- 使用数组进行增加/删除元素的示意代码—比较麻烦
- 写出Person数组扩容示意代码
Person[] p1 = new Person[1];
p1[0] = new Person();
//增加新的Person对象
Person[] p2 = new Person[p1.length];//新建数组
for(){}//拷贝p1数组的元素到p2
p2[p2.length-1] = new Person();//添加新的对象
- 因此,我们就用到了集合,集合有很多有点
- 可以动态保存任意多个对象,使用比较方便
- 提供了一系列方便的操作对象的方法:add、remove、set、get等
- 使用集合添加/删除新元素的示意代码–简洁明了
1. 集合框架体系
- Java的集合类有很多,主要分为两大类(单列集合、双列集合)
- collection接口有两个重要的接口 List Set ,他们实现的子类都是单列集合
- Map接口的实现子类 是双列集合,存放 K-V
- 详见下图:
2. Collection 接口和常用方法
2.1 Collection 接口实现类的特点
public interface Collection extends Iterable
- collection实现子类可以存放多个元素,每个元素可以是Object
- Collection的实现类,有些可以存放重复的元素,有些不可以
- Collection的实现类,有些是有序的(List),有些不是有序的(Set)
- Collection接口没有直接的实现子类,是通过他的子接口Set和List来实现的
- 因为Collection接口常用方法有很多,这里以实现子类 ArrayList 来演示
- add 添加单个元素
- remove 删除指定元素
- contains 查找某个元素是否存在
- size 获取元素个数
- isEmpty 判断是否为空
- clear 清空
- addAll 添加多个元素
- containsAll 查找多个元素是否都存在
- removeAll 删除多个元素
- 代码如下:
public static void main(String[] args) {
//以接口来接收 List
List list = new ArrayList();
//1. add 添加单个元素
list.add("jack");
list.add(10);
list.add(true);
System.out.println("List = "+list);//List = [jack, 10, true]
//2. remove 删除指定元素
list.remove(0);//指定删除第一个元素
list.remove(true);//指定删除true
System.out.println("List = "+list);//List = [10]
//3. contains 查找某个元素是否存在
//这里就直接放在一个代码表示了
System.out.println(list.contains(10));//存在元素 0 所以输出true
//4. size 获取元素个数
System.out.println(list.size());//1
//5. isEmpty 判断是否为空
System.out.println(list.isEmpty());//false
//6. clear 清空
list.clear();
System.out.println("List = "+list);//List = []
//7. addAll 添加多个元素
ArrayList list2 = new ArrayList();
list2.add("金瓶梅");
list2.add("贾宝玉");
list.addAll(list2);
System.out.println("List = "+list);//List = [金瓶梅, 贾宝玉]
//8. containsAll 查找多个元素是否都存在
System.out.println(list.containsAll(list2));//true
//8. removeAll 删除多个元素
list.add("西游记");
list.add("武松");
System.out.println("List = "+list);//List = [金瓶梅, 贾宝玉, 西游记, 武松]
list.removeAll(list2);
System.out.println("List = "+list);//List = [西游记, 武松]
}
2.2 Collection 接口遍历元素方式 1——使用Iterator(迭代器)
2.2.1 基本介绍
- 我们都知道Collection的子类都是实现了Collection
- 但是Collection上面有一个父接口 Iterable
- 我们看 Iterable 源码
public interface Iterable<T> {
/**
* Returns an iterator over elements of type {@code T}.//返回T类型元素的迭代器。
*
* @return an Iterator.//@返回一个迭代器
*/
Iterator<T> iterator();
-
看源码所得:
-
Iterator 对象称为迭代器,主要用于遍历 Collection 集合中的元素。
-
所有实现了 Collection 接口的集合类都有一个iterator() 方法,用以返回一个实现了 Iterator 接口的对象,即可以返回一个迭代器
-
Iterator 的结构图见迭代器的执行原理:
-
Iterator 仅用于遍历集合,Iterator 本身不存放对象
-
2.2.2 迭代器的执行原理
-
迭代器的执行原理
-
Iterator iterator = coil.iterator();//得到一个集合的迭代器
-
//hasNext(); 判断是否还有下一个元素
-
while(iterator.hasNext()){
-
//next()作用: ①指针下移②将下移以后集合位置上的元素返回
-
System.out.println(iterator.next());
-
}
-
2.2.3 Iterator 接口的方法
- hasNext()
- next()
- remove
- 如下图
- 调用Iterator 接口的方法时的注意点
- 调用 Iterator.next() 方法之前必须调用 iterator.hasNext() 方法进行检测。
- 如果不调用,且下一条记录无效,直接调用 Iterator.next() 会抛出异常
- NoSuchElementException异常
- 下面对遍历集合进行简单的代码操作:
public class CollectionIterator {
public static void main(String[] args) {
Collection col = new ArrayList();
col.add(new Book("小李飞刀", "古龙", 66));
col.add(new Book("笑傲江湖", "金庸", 68));
col.add(new Book("红楼梦", "曹雪芹", 55));
System.out.println(col);
//现在希望能够遍历 col 集合
//1. 先得到 col 对于的迭代器
Iterator iterator = col.iterator();
//2. 使用 while 循环遍历
while (iterator.hasNext()) {
//返回下一个元素,类型是object
Object next = iterator.next();
System.out.println(next);
}
/*
//显示所有快捷键的快捷键 ctrl + j
//正常写while循环太慢了,这里有个快捷键 itit
while (iterator.hasNext()) {
Object next = iterator.next();
}
*/
//3. 当退出 while 循环后,这时Iterator迭代器,指向最后的元素
//iterator.next();//NoSuchElementException 无此类元素异常
//4. 如果需要再次遍历,需要重置我们的迭代器
iterator = col.iterator();//重置迭代器
System.out.println("=======第二次遍历========");
//再次遍历 col 集合
while (iterator.hasNext()) {
Object next = iterator.next();
System.out.println(next);
}
}
}
class Book {
String name;
String author;
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 +
'}';
}
}
2.3 Collection 接口遍历对象方式 2——增强for 循环
2.3.1 基本介绍
- 增强for循环可以替代Iterator迭代器:增强for就是简化版的Iterator;本质是一样的,只能用于遍历集合或数组
- 基本语法
for(元素类型 元素名 : 集合名或者数组名){
访问元素
}
- 案例演示
public class CollectionFor {
public static void main(String[] args) {
//抑制错误提示
@SuppressWarnings({"all"})
Collection col = new ArrayList();
col.add(new Book("小李飞刀", "古龙", 66));
col.add(new Book("笑傲江湖", "金庸", 68));
col.add(new Book("红楼梦", "曹雪芹", 55));
System.out.println(col);//原始输出
//增强for循环
//1. 增强for ,在collection集合
//2. 增强for , 仍然是迭代器
//3. 增强for ,就是简化版本的迭代器遍历
//4. 这里的快捷键 I
/*
for(元素类型 元素名 : 集合名或者数组名){
访问元素
}
*/
for (Object book : col) {
System.out.println("book = " + book);
}
}
}
2.4 练习题
2.4.1 练习题1
- 创建 3 个 Dog {name, age} 对象,放入到 ArrayList 中,赋给 List 引用
- 用迭代器和增强 for 循环两种方式来遍历
- 重写 Dog 的 toString 方法, 输出 name 和 age
- 代码如下:
public class CollectionExercise01 {
public static void main(String[] args) {
List list = new ArrayList();
list.add(new Dog("旺财", 3));
list.add(new Dog("来福", 5));
list.add(new Dog("富贵", 10));
System.out.println("=======迭代器=======");
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
Object dog = iterator.next();
System.out.println(dog);
}
System.out.println("=======增强for=======");
for (Object dog : list) {
System.out.println(dog);
}
}
}
class Dog {
/*
1. 创建 3 个 Dog {name, age} 对象,放入到 ArrayList 中,赋给 List 引用
2. 用迭代器和增强 for 循环两种方式来遍历
3. 重写 Dog 的 toString 方法, 输出 name 和 age
*/
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;
}
//重写子类toString
@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
3. List 接口和常用方法
3.1 List 接口基本介绍
- List 接口是 Collection 接口的子接口
- List 集合类中元素有序(即添加顺序和取出顺序一致)、且可以重复;
- List集合中的每个元素都有其对应的顺序索引,即支持索引
- List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素
- JDK API 中List 接口的实现类有很多:
3.2 List 接口的常用方法
- List 接口的方法有特别多
- 这里列举几个常用的方法
- 详见代码
public class ListMethod {
public static void main(String[] args) {
List list = new ArrayList();
list.add("张飞");
list.add("关羽");
list.add("lucy");
//1. void add(int index, Object ele):在 index 位置插入 ele 元素
//在 index = 1 的位置插入一个对象 刘备
list.add(1, "刘备");
System.out.println("List = " + list);
//输出 List = [张飞, 刘备, 关羽, lucy]
//2. boolean addAll(int index, Collection eles):从 index 位置开始将 eles 中的所有元素添加进来
List list2 = new ArrayList();
list2.add("Jack");
list2.add("lucy");
list2.add("Tom");
//在 list 的 index = 1 的位置插入所有 list2 的对象
list.addAll(1, list2);
System.out.println("List = " + list);
//输出 List = [张飞, Jack, lucy, Tom, 刘备, 关羽, lucy]
//3. Object get(int index):获取指定 index 位置的元素
//获取指定第三个位置的元素
System.out.println(list.get(2));//输出 lucy
//4. int indexOf(Object obj):返回 obj 在集合中首次出现的位置
System.out.println(list.indexOf("lucy"));//输出 2
//5. int lastIndexOf(Object obj):返回 obj 在当前集合中末次出现的位置
System.out.println(list.lastIndexOf("lucy"));//输出 6
//6. Object remove(int index):移除指定 index 位置的元素,并返回此元素
list.remove(0);//移除第 0 个位置的元素
System.out.println("List = " + list);
//输出 List = [Jack, lucy, Tom, 刘备, 关羽, lucy]
//7. Object set(int index, Object ele):设置指定 index 位置的元素为 ele , 相当于是替换.
list.set(1, "Andy");//将第二个位置的元素 lucy 替换成了 Andy
System.out.println("List = " + list);
//输出 List = [Jack, Andy, Tom, 刘备, 关羽, lucy]
//8. List subList(int fromIndex, int toIndex):返回从 fromIndex 到 toIndex 位置的子集合
// 注意返回的子集合 fromIndex <= subList < toIndex
List returnList = list.subList(0, 3);//[0,3)
System.out.println("returnList =" + returnList);
//输出 returnList =[Jack, Andy, Tom]
}
}
3.2.1 练习
要求:
- 添加 10 个以上的元素(比如 String “hello” );
- 在 3 号位插入一个元素"营养快线",
- 获得第 5 个元素;
- 删除第 6 个元素;
- 修改第 7 个元素;
- 在使用迭代器遍历集合;
- 要求使用 List 的实现类 ArrayList 完成。
- 代码如下:
public class ListExercise {
public static void main(String[] args) {
/*
//1. 添加 10 个以上的元素
//2. 在 3 号位插入一个元素"营养快线",
//3. 获得第 5 个元素;
//4. 删除第 6 个元素;
//5. 修改第 7 个元素;
//6. 在使用迭代器遍历集合;
*/
//1. 添加 10 个以上的元素(比如 String "hello" );
List list = new ArrayList();
list.add("张飞");
list.add("吕布");
list.add("刘备");
list.add("关羽");
list.add("赵云");
list.add("孙尚香");
list.add("貂蝉");
list.add("大乔");
list.add("小乔");
list.add("纲手");
System.out.println("list ="+list);
//输出 list =[张飞, 吕布, 刘备, 关羽, 赵云, 孙尚香, 貂蝉, 大乔, 小乔, 纲手]
//2. 在 3 号位插入一个元素"营养快线",
list.add(2,"营养快线");
System.out.println("list ="+list);
//输出 list =[张飞, 吕布, 营养快线, 刘备, 关羽, 赵云, 孙尚香, 貂蝉, 大乔, 小乔, 纲手]
//3. 获得第 5 个元素;
System.out.println(list.get(4));//输出 关羽
//4. 删除第 6 个元素;
list.remove(5);
System.out.println("list ="+list);
//输出 list =[张飞, 吕布, 营养快线, 刘备, 关羽, 孙尚香, 貂蝉, 大乔, 小乔, 纲手]
//5. 修改第 7 个元素;
list.set(6,"娃哈哈");
System.out.println("list ="+list);
//输出 list =[张飞, 吕布, 营养快线, 刘备, 关羽, 孙尚香, 娃哈哈, 大乔, 小乔, 纲手]
//6. 在使用迭代器遍历集合;
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
Object newList = iterator.next();
System.out.println("newList ="+newList);
}
/* 输出
newList =张飞
newList =吕布
newList =营养快线
newList =刘备
newList =关羽
newList =孙尚香
newList =娃哈哈
newList =大乔
newList =小乔
newList =纲手
*/
}
}
3.3 List 的三种遍历方式
- 方式一:使用迭代器 iterator
- 方式二:使用增强for循环
- 方式三:使用普通for循环
public class ListFor {
public static void main(String[] args) {
//List接口的实现子类 Vector LinkedList
//List list = new Vector();
//List list = new LinkedList();
List list = new ArrayList();
list.add("张飞");
list.add("吕布");
list.add("刘备");
list.add("关羽");
list.add("赵云");
System.out.println("list =" + list);
//输出 list =[张飞, 吕布, 刘备, 关羽, 赵云]
//1. 方式一:使用迭代器 iterator
System.out.println("======迭代器=====");
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
Object obj = iterator.next();
System.out.println("obj = " + obj);
}
/*输出
======迭代器=====
obj = 张飞
obj = 吕布
obj = 刘备
obj = 关羽
obj = 赵云
*/
//2. 方式二:使用增强for循环
System.out.println("======增强for=====");
for (Object o : list) {
System.out.println("o = " + o);
}
/*输出
======增强for=====
o = 张飞
o = 吕布
o = 刘备
o = 关羽
o = 赵云
*/
//3. 方式三:使用普通for循环
System.out.println("======普通for=====");
for (int i = 0; i < list.size(); i++) {
System.out.println("对象 = " + list.get(i));
}
/*输出
======普通for=====
对象 = 张飞
对象 = 吕布
对象 = 刘备
对象 = 关羽
对象 = 赵云
*/
}
}
3.3.1 练习题
-
使用List的实现类添加三本图书,并遍历,打印效果如下:
名称:XX 价格:XX 作者:XX
名称:XX 价格:XX 作者:XX
名称:XX 价格:XX 作者:XX
-
要求:按照价格排序,从低到高(使用冒泡法)
-
要求使用ArrayList、LinkedList和Vector三种集合实现
-
代码如下:
public class ListExercise02 {
public static void main(String[] args) {
//3. 要求使用ArrayList、LinkedList和Vector三种集合实现
//使用Vector集合实现 List list = new Vector();
//使用LinkedList集合实现 List list = new LinkedList();
List list = new ArrayList();
list.add(new Book("西游记", 66, "吴承恩"));
list.add(new Book("红楼梦", 55, "曹雪芹"));
list.add(new Book("水浒传", 58, "施耐庵"));
//1. 使用List的实现类添加三本图书,并遍历:
for (Object o : list) {
System.out.println(o);
}
System.out.println("======排序后=======");
//2. 要求:按照价格排序,从低到高(使用冒泡法)
sort(list);
for (Object o : list) {
System.out.println(o);
}
}
//先创建一个方法,使用冒泡排序方法进行排序
public static void sort(List list) {
int listSize = list.size();
for (int i = 0; i < listSize - 1; i++) {
for (int j = 0; j < listSize - i - 1; j++) {
//取出对象Book
Book book1 = (Book) list.get(j);
Book book2 = (Book) list.get(j + 1);
double temp = 0;
if (book1.getPrice() > book2.getPrice()) {
//集合直接用set方法就进行元素位置交换
list.set(j, book2);
list.set(j + 1, book1);
}
}
}
}
}
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 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;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
@Override
public String toString() {
return "名称:" + name + "\t\t" +
"价格:" + price + "\t\t" +
"作者:" + author;
}
}
4. ArrayList 底层结构和源码分析
4.1 注意事项
- permits all elements , including null , ArrayList 可以加入null , 并且多个
- ArrayList 是由数组来实现数据储存的
- ArrayList 基本等同于 Vector , 除了 ArrayList 是线程不安全(执行效率高)
- 在多线程的情况下,不建议使用ArrayList
public class ArrayListDetail {
public static void main(String[] args) {
//ArrayList 是线程不安全,看源码,没有 synchronized 关键字
/*
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
*/
ArrayList arrayList = new ArrayList();
arrayList.add(null);//可以加入null
arrayList.add("Jack");
System.out.println(arrayList);
}
}
4.2 ArrayList 的底层操作机制源码分析(重点、难点)
- ArrayList中维护了一个Object类型的数组elementData [debug 看源码]
transient Object[] elementData; //transient表示瞬间,短暂的,表示该属性不会被序列号 - 当创建ArrayList对象时,如果使用的是无参构造器,则初始elementData容量为0,第1
次添加,则扩容elementData为10,如需要再次扩容,则扩容elementData为1.5倍。 - 如果使用的是指定大小的构造器,则初始elementData容量为指定大小,如果需要扩容,
则直接扩容elementData为1.5倍。
- 建议大家自己来Debug一下ArrayList容量的创建和扩容的流程
public class ArrayListSource {
public static void main(String[] args) {
//使用无参构造器创建ArrayList对象
ArrayList list = new ArrayList();
//使用for循环给list集合添加1-10数据
for (int i = 1; i <= 10; i++) {
list.add(i);
}
//使用for循环给list集合添加11-15数据
for (int i = 11; i <= 15; i++) {
list.add(i);
}
list.add(100);
list.add(200);
list.add(null);
}
}
5. Vector 底层结构和源码剖析
5.1 Vector 的基本介绍
- Vector类定义说明
public class Vector<E>
extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
- Vector底层也是一个对象数组,protected Object[] elementData;
- Vector是线程同步的,即线程安全的,Vector类的操作方法基本都带有Synchronized关键字
- 因此在开发中,需要线程同步安全时,考虑使用Vector
Vector底层结构和ArrayList的比较
-
底层结构都是可变数组
-
版本:
- ArrayList是jdk1.2以后出现的
- Vector是jdk1.0以后出现的
-
线程安全(同步)效率
- ArrayList是不安全的,但是效率高
- Vector是安全的,但是效率不高
-
扩容倍数
- ArrayList:
- 有参构造一开始指定,后面按照1.5倍扩容
- 无参构造第一次是10,第二次开始按照1.5倍扩容
- Vector
- 有参第一次指定,后面按照2倍扩容
- 无参构造默认是10,后面按照2倍扩容
- ArrayList:
-
建议大家自己来Debug一下Vector容量的创建和扩容的流程,下面拿vector无参构造器演示一下:
public class Vector_ {
public static void main(String[] args) {
//无参构造
Vector vector = new Vector();
//普通for循环增加元素
for (int i = 0; i < 10; i++) {
vector.add(i);
}
vector.add(100);
//1. new Vector() 底层
/*
public Vector() {
this(10);//容量为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 确定是否需要扩容
/*
private void ensureCapacityHelper(int minCapacity) {
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
*/
//3. 如果需要的数组大小不够用了,就进行扩容
// 扩容的算法:
// int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
// capacityIncrement : oldCapacity);
//简而言之就是扩容2倍
//有参构造器开始指定容量,后面算法和无参的一样
/*
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);
}
*/
}
}
6. LinkedList 底层结构
6.1 LinkedList 的全面说明
- LinkedList底层实现了双向链表和双端队列特点
- 可以添加任意元素(元素可以重复),包括null
- 线程不安全,没有实现同步
6.2 LinkedList底层结构
6.2.1 基本介绍
- LinkedList底层维护了一个双向链表
- LinkedList中维护了两个属性first和last分别指向首节点和尾结点
- 每个节点(Node对象)里面维护了prev、next、item三个属性,其中通过prev指向前一个,通过next指向后一个节点,最终实现双向链表
- 所以LinkedList的元素的添加和删除不是通过数组完成的,相对来说效率比较高。
- 示意图如下
- 下面用代码模拟一个简单的双向链表:
public class LinkedList01 {
public static void main(String[] args) {
//模拟一个简单的双向链表
Node jack = new Node("jack");
Node tom = new Node("tom");
Node lucy = new Node("lucy");
//链接三个结点,形成双向链表
//jack -> tom -> lucy
jack.next = tom;
tom.next = lucy;
//lucy -> tom -> jack
lucy.pre = tom;
tom.pre = jack;
//让 first 引用指向 Jack ,就是双向链表的头结点
Node first = jack;
//让 last 引用指向 Jack ,就是双向链表的尾结点
Node last = lucy;
//演示:从头到尾进行遍历
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;
}
//演示双向链表添加/删除对象
//在 tom 和 Lucy 之间插入一个对象 张飞 ,名字为 name
//1. 先创建一个Node结点
Node name = new Node("张飞");
//2.先把 tom 的 next 指向 name ,name 的 next 指向 lucy
//从而重新形成链接 jack -> tom -> name -> lucy
tom.next = name;
name.next = lucy;
//同理把pre也重新链接
//lucy -> name -> tom -> jack
lucy.pre = name;
name.pre = tom;
//让first再次指向jack ,形成双向链表的头结点
first = jack;
//演示:从头到尾进行遍历新的链表
System.out.println("===从头到尾进行遍历新的链表===");
while (true) {
if (first == null) {
break;
}
System.out.println(first);
first = first.next;
}
//让 last 引用重新指向 Jack ,形成双向链表的尾结点
last = lucy;
//演示:从尾到头进行遍历新的链表
System.out.println("===从尾到头进行遍历新的链表===");
while (true) {
if (last == null) {
break;
}
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;
}
public String toString() {
return "Node name=" + item;
}
}
6.2.2 LinkedList 的增删改查案例
public class LinkedListCRUD {
public static void main(String[] args) {
LinkedList linkedList = new LinkedList();
//1. 演示一个添加的源码阅读
linkedList.add(1);
linkedList.add(2);
linkedList.add(3);
System.out.println("=====最初的LinkedList=====");
System.out.println("LinkedList = " + linkedList);
//2. 演示一个删除的源码阅读
linkedList.remove();//默认删除第一个结点
System.out.println("=====删除后=====");
System.out.println("LinkedList = " + linkedList);
//3. 修改某个结点对象
System.out.println("=====修改后=====");
linkedList.set(1, 999);//将第二个元素改成999
System.out.println("LinkedList = " + linkedList);
//4. 得到某个结点对象
//get(1) 得到双向链表的第二个对象
System.out.println(linkedList.get(1));//输出 999
//遍历
//因为 LinkedList 实现了List的接口,
//所以遍历方式跟前面一样
System.out.println("=====迭代器遍历=====");
Iterator iterator = linkedList.iterator();
while (iterator.hasNext()) {
Object next = iterator.next();
System.out.println(next);
}
System.out.println("=====增强for循环遍历=====");
for (Object o : linkedList) {
System.out.println(o);
}
System.out.println("=====普通for循环遍历=====");
for (int i = 0; i < linkedList.size(); i++) {
System.out.println(linkedList.get(i));
}
// 添加的源码阅读
//1. LinkedList linkedList = new LinkedList();
/* public LinkedList() {
}
*/
//2. 这时 LinkedList 的属性为空
// first = null ; last = null;
//3. 执行 添加
/*
public boolean add(E e) {
linkLast(e);
return true;
}
*/
//4. 将新的结点加入到双向链表的最后
/*
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++;
}
*/
//删除的源码阅读
//linkedList.remove();
//1. 执行removeFirst();
/*
public E remove() {
return removeFirst();
}
*/
//2. 执行下面的方法
/*
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
*/
//3. 执行 unlinkFirst ,将 f 指向双向链表的第一个结点拿掉
/*
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
final E element = f.item;
final Node<E> next = f.next;
f.item = null;
f.next = null; // help GC
first = next;
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}
*/
}
}
6.3. ArrayList 和 LinkedList 比较
- 底层结构:
- ArrayList 的底层结构是可变数组
- LinkedList 的底层结构是双向链表
- 增删的效率
- ArrayList 增删的效率较低 [ 数组的扩容 ]
- LinkedList 增删的效率较高 [ 通过链表追加 ]
- 改查的效率
- ArrayList 改查的效率较高
- LinkedList 改查的效率较低
- 那么我们该如何选择ArrayList和LinkedList呢?
- 如果我们改查的操作多,选择ArrayList
- 如果我们增删的操作多,选择LinkedList
- 一般来说,在程序中,80%-90%都是查询,因此大部分的情况下都会选择ArrayList
- 在一个项目中,根据业务灵活选择,也可能这样,一个模块使用的是ArrayList,另外一个模块是LinkedList
- 因此,我们要根据实际情况来选择。
7. Set 接口和常用方法
7.1 Set 接口基本介绍
- Set接口是无序的(添加和取出的顺序不一致),没有索引;
- 不允许重复元素,所以最多包含一个null;
- JDK API中set接口的实现类也有很多;
7.2 Set 接口的常用方法
- 和 List 接口一样, Set 接口也是 Collection 的子接口,因此,常用方法和 Collection 接口一样
7.3 Set 接口的遍历方式
- 同Collection的遍历方式一样,因为Set接口是Collection接口的子接口
- 使用迭代器进行遍历
- 增强for循环进行遍历
- 不能使用索引的方式来获取
public class SteMethod {
public static void main(String[] args) {
//1. 以 Set 接口的实现类 HashSet 来讲解 Set 接口的方法
//2. Set接口的实现类的对象(Set接口对象),不能存放重复的元素,可以添加一个null
//3. Set接口存放数据是无序的(即添加的顺序和取出的顺序是不一致的)
//4. 注意:取出的顺序的顺序虽然不是添加的顺序,但是他是固定的
Set set = new HashSet();
set.add("jack");
set.add("lucy");
set.add("tom");
set.add("lucy");
set.add("sun");
set.add(null);
set.add(null);
System.out.println(set);
//输出 [null, tom, lucy, sun, jack]
//遍历
//1. 迭代器进行遍历
System.out.println("=====迭代器进行遍历=====");
Iterator iterator = set.iterator();
while (iterator.hasNext()) {
Object next = iterator.next();
System.out.println("next = " + next);
}
//删除
set.remove(null);
//2. 增强for循环进行遍历
System.out.println("=====增强for循环进行遍历=====");
for (Object o : set) {
System.out.println("o = " + o);
}
//Set 接口对象不能通过索引来获取
//没有set.get()方法
}
}
8. Set接口实现类——HashSet
8.1 HashSet的全面说明
-
HashSet 实现了Set接口
-
HashSet 实际上是HashMap(看源码)
-
HashSet 可以存放null值,但是只能有一个null
-
HashSet 不保证元素是有序的,取决于hash后,再确定索引的结果(即不保证元素的存放顺序和取出顺序是一致的)
-
HashSet 不能有重复的元素/对象
public class HashSet_ {
public static void main(String[] args) {
//1. 构造器走的源码
/*
public HashSet() {
map = new HashMap<>();
}
*/
//2. HashSet 可以存放null值,但是只能有一个null
Set hashSet = new HashSet();
hashSet.add(null);
hashSet.add("jack");
hashSet.add("lucy");
hashSet.add(null);
hashSet.add("tom");
System.out.println(hashSet);
//输出 [null, tom, lucy, jack]
}
}
- 案例
public class HashSet01 {
public static void main(String[] args) {
//1. 执行一个 add 方法后,会返回一个 boolean 值
//2. 如果添加成功,会返回 true ,否则返回 false
//3. 可以通过 remove 指定删除哪个对象
Set set = new HashSet();
System.out.println(set.add("jack"));//true
System.out.println(set.add("lucy"));//true
System.out.println(set.add("tom"));//true
System.out.println(set.add("sun"));//true
System.out.println(set.add("jack"));//false
set.remove("tom");
System.out.println("set = " + set);//3个
//输出 set = [lucy, sun, jack]
//4. HashSet 不能添加相同的元素/数据
set = new HashSet();
set.add("lucy");
set.add("lucy");
//下面两个是不同的对象,可以都存放进去
set.add(new Dog("tom"));//OK
set.add(new Dog("tom"));//OK
System.out.println("set = " + set);//3个
//输出 set = [Dog{name='tom'}, Dog{name='tom'}, lucy]
//再加深一下
//后面进行源码分析
//add 到底发生了什么 ===> 后面看底层机制
set.add(new String("sun"));//ok
set.add(new String("sun"));//加入不了
System.out.println("set = " + set);//4个
//输出 set = [Dog{name='tom'}, Dog{name='tom'}, lucy, sun]
}
}
class Dog {//定义Dog类
private String name;
public Dog(String name) {
this.name = name;
}
@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +
'}';
}
}
9. Set接口实现类——HashSet
9.1 HashSet底层机制说明
- 分析HashSet底层是HashMap,HashMap底层是(数组+链表+红黑树)
- 下面模拟简单的数组+链表结构
public class HashSetStructure {
public static void main(String[] args) {
//模拟一个HashSet的底层(HashMap底层机制)
//1. 创建一个数组,数组的类型是Node[]
//2. 有些人直接把 Node[] 数组称为表table
Node[] table = new Node[16];
//3. 创建结点
Node jack = new Node("jack", null);
table[2] = jack;
Node lucy = new Node("lucy", null);
jack.next = lucy;//将lucy挂载到jack
Node tom = new Node("tom", null);
lucy.next = tom;
//4. 创建新的结点
Node rose = new Node("rose", null);
table[3] = rose;
System.out.println("table = " + table);
}
}
class Node {//结点,存储数据,可以指向下一个结点,从而形成链表
Object item;//存放数据
Node next;//指向下一个结点
public Node(Object item, Node next) {
this.item = item;
this.next = next;
}
}
- 示意图
-
分析HashSet的添加元素底层是如何实现的(hash()+equals())
- HashSet底层是HashMap
- 添加一个元素时,先得到Hash值—>会转成—>索引值
- 找到存储数据表 table ,看这个索引位置是够已经存放的元素
- 如果没有就直接加入
- 如果有,调用 equals 比较,如果相同,就放弃添加,如果不相同,则添加到最后
- 在 Java8 中,如果一条链表的元素个数到达 TREEIFY_THRESHOLD (默认是8),并且 table 的大小 >= MIN_TREEIFY_CAPACITY (默认是64),就会进行树化(红黑树)。
- 下面进行代码分析
public class HashSetSource { public static void main(String[] args) { //1. 先执行的是一个构造器 // public HashSet() /* public HashSet() { map = new HashMap<>(); } */ HashSet hashSet = new HashSet(); hashSet.add("jack"); hashSet.add("lucy"); hashSet.add("jack"); hashSet.add("tom"); System.out.println("set = " + hashSet); //2. 执行 add() /* public boolean add(E e) {//e = "jack" value = PRESENT return map.put(e, PRESENT)==null;//(static) PRESENT = new Object(); } */ //3. 执行 put() 该方法会执行 hash(key) 得到 key 对应的 hash 值 算法 h = key.hashCode()) ^ (h >>> 16) /* public V put(K key, V value) {//key = jack valeu = PRESENT(共享) return putVal(hash(key), key, value, false, true); } */ //4. 执行 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 // 就是第一次扩容,到 16 个空间. if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //(1)根据 key,得到 hash 去计算该 key 应该存放到 table 表的哪个索引位置 // 并把这个位置的对象,赋给 p //(2)判断 p 是否为 null //(2.1) 如果 p 为 null, 表示还没有存放元素, 就创建一个 Node (key="java",value=PRESENT) //(2.2) 就放在该位置 tab[i] = newNode(hash, key, value, null) 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) 准备加入的 key 和 p 指向的 Node 结点的 key 是同一个对象 // (2) p 指向的 Node 结点的 key 的 equals() 和准备加入的 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); else {//如果 table 对应索引位置,已经是一个链表, 就使用 for 循环比较 //(1) 依次和该链表的每一个元素比较后,都不相同, 则加入到该链表的最后 // 注意在把元素添加到链表后,立即判断 该链表是否已经达到 8 个结点 // , 就调用 treeifyBin() 对当前这个链表进行树化(转成红黑树) // 注意,在转成红黑树时,要进行判断, 判断条件 // if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY(64)) // resize(); // 如果上面条件成立,先 table 扩容. // 只有上面条件不成立时,才进行转成红黑树 //(2) 依次和该链表的每一个元素比较过程中,如果有相同情况,就直接 break for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); 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; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; //size 就是我们每加入一个结点 Node(k,v,h,next), size++ if (++size > threshold) resize();//扩容 afterNodeInsertion(evict); return null; } */ } }
-
分析HashSet的扩容和转成红黑树机制
- Hash底层是HashMap,第一次添加时,table数组扩容到16,临界值(threshold)是16*加载因子(loadFactor)是0.75=12
- 如果table数组使用到了临界值12,就会扩容到162=32,新的临界值就是320.75=24,以此类推
- 在Java8中,如果一条链表的元素个数到达TREEIFY_THRESHOLD(默认是8),并且table的大小 >= MIN_TREEIFY_CAPACITY(默认是64),就会进行树化(红黑树),否则仍然采用数组扩容机制。
public class HashSetIncrement { public static void main(String[] args) { /* 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 = 1; i <= 100; i++) { // hashSet.add(i);//1,2,3,4,5...100 //} /* 在 Java8 中, 如果一条链表的元素个数到达 TREEIFY_THRESHOLD(默认是 8 ), 并且 table 的大小 >= MIN_TREEIFY_CAPACITY(默认 64),就会进行树化(红黑树), 否则仍然采用数组扩容机制 */ //代码演 //for (int i = 1; i <= 12; i++) { // hashSet.add(new A(i));// //} /* 当我们向 hashset 增加一个元素,-> Node -> 加入 table , 就算是增加了一个 size++ */ //代码演示 for (int i = 1; i <= 7; i++) {//在 table 的某一条链表上添加了 7 个 A 对象 hashSet.add(new A(i));// } for (int i = 1; i <= 7; i++) {//在 table 的另外一条链表上添加了 7 个 B 对象 hashSet.add(new B(i));// } } } class B { private int n; public B(int n) { this.n = n; } @Override public int hashCode() { return 200; } } class A { private int n; public A(int n) { this.n = n; } @Override public int hashCode() { return 100; } }
9.2 HashSet 课堂练习
练习1
- 定义一个Employee类,该类包含:private成员属性name,age;
- 要求:
- 创建3个Employee对象放入HashSet中
- 当name和age的值相同时,认为是相同员工,不能添加到HashSet集合中
public class HashSetExercise01 {
public static void main(String[] args) {
HashSet hashSet = new HashSet();
hashSet.add(new Employee("jack", 15));
hashSet.add(new Employee("lucy", 18));
hashSet.add(new Employee("tom", 20));
hashSet.add(new Employee("jack", 15));//加入不成功
System.out.println("hashSet =" + hashSet);
//输出 [Employee{name='jack', age=15},
// Employee{name='tom', age=20},
// Employee{name='lucy', age=18}]
}
}
/*
1. 定义一个Employee类,该类包含:private成员属性name,age;
2. 要求:
- 创建3个Employee对象放入HashSet中
- 当name和age的值相同时,认为是相同员工,不能添加到HashSet集合中
*/
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 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 "Employee{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
//如果 name 和 age 值相同,则返回相同的 hash 值
@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.age && Objects.equals(name, employee.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
练习2
-
定义一个Employee类,该类包含:private成员属性name,sal,birthday(MyDate类型);
-
其中 birthday为MyDate类型(属性包括:year,month,day);
-
要求:
- 创建3个Employee放入HashSet中
- 当name和birthday的值相同时,认为是相同员工,不能添加到HashSet集合中
-
思路:
- ① 因为Employee类里面的birthday要用MyDate类,所以先定义好 MyDate 类:
public class MyDate { private int year; private int month; private int day; public MyDate(int year, int month, int day) { this.year = year; this.month = month; this.day = day; } public int getYear() { return year; } public void setYear(int year) { this.year = year; } public int getMonth() { return month; } public void setMonth(int month) { this.month = month; } public int getDay() { return day; } public void setDay(int day) { this.day = day; } //重写 toString 方法,方便输出 @Override public String toString() { return "MyDate{" + "year=" + year + ", month=" + month + ", day=" + day + '}'; } //重写 equals 和 hashCode 方法 @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; MyDate myDate = (MyDate) o; return year == myDate.year && month == myDate.month && day == myDate.day; } @Override public int hashCode() { return Objects.hash(year, month, day); } }
- ② 定义Employee类
public class Employee { private String name; private double sal; private MyDate birthday; public Employee(String name, double sal, MyDate birthday) { this.name = name; this.sal = sal; this.birthday = birthday; } public String getName() { return name; } public void setName(String name) { this.name = name; } public double getSal() { return sal; } public void setSal(double sal) { this.sal = sal; } public MyDate getBirthday() { return birthday; } public void setBirthday(MyDate birthday) { this.birthday = birthday; } //重写 toString 方法,方便输出 @Override public String toString() { return "\nEmployee{" + "name='" + name + '\'' + ", sal=" + sal + ", birthday=" + birthday + '}'; } //重写 name 和 birthday 的 equals 和 hashCode 方法 @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Employee employee = (Employee) o; return Objects.equals(name, employee.name) && Objects.equals(birthday, employee.birthday); } @Override public int hashCode() { return Objects.hash(name, birthday); } }
- 最后创建HashSet类,在里面放入Employee
- 因为要对name和birthday的值进行比较,所以我们重写 name 和 birthday 的 equals 和 hashCode 方法
public class HashSetExercise02 { public static void main(String[] args) { /* 1. 定义一个Employee类,该类包含:private成员属性name,sal,birthday(MyDate类型); 2. 其中 birthday为MyDate类型(属性包括:year,month,day); 3. 要求: - 创建3个Employee放入HashSet中 - 当name和birthday的值相同时,认为是相同员工,不能添加到HashSet集合中 */ HashSet hashSet = new HashSet(); hashSet.add(new Employee("jack", 20000, new MyDate(1988, 11, 26))); hashSet.add(new Employee("lucy", 18000, new MyDate(1990, 12, 26))); hashSet.add(new Employee("tom", 15000, new MyDate(1993, 11, 26))); hashSet.add(new Employee("jack", 16000, new MyDate(1988, 11, 26)));//添加失败 System.out.println("HashSet = " + hashSet); //输出 HashSet = [ // Employee{name='tom', sal=15000.0, birthday=MyDate{year=1993, month=11, day=26}}, // Employee{name='lucy', sal=18000.0, birthday=MyDate{year=1990, month=12, day=26}}, // Employee{name='jack', sal=20000.0, birthday=MyDate{year=1988, month=11, day=26}}] } }
10. Set 接口实现类-LinkedHashSe
10.1 LinkedHashSet 的全面说明
- LinkedHashSet是HashSet的子类
- LinkedHashSet底层是一个LinkedHashMap,底层维护了一个数组+双向链表
- LinkedHashSet根据元素的 hashCode值来决定元素的存储位置,同时使用链表维护元素的次序(图),这使得元素看起来是以插入顺序保存的。
- LinkedHashSet 不允许添重复元素
10.2 LinkedHashSet 练习
- Car类(属性name,price)
- 如果两个Car的name和price一样,则认为是相同元素,就不能添加
public class LinkedHashSetExercise {
public static void main(String[] args) {
LinkedHashSet linkedHashSet = new LinkedHashSet();
linkedHashSet.add(new Car("雷克萨斯", 400000));
linkedHashSet.add(new Car("奔驰", 500000));
linkedHashSet.add(new Car("宝马", 600000));
linkedHashSet.add(new Car("保时捷", 800000));
linkedHashSet.add(new Car("雷克萨斯", 400000));//添加失败
System.out.println(linkedHashSet);
//输出[
// Car{name='雷克萨斯', price=400000},
// Car{name='奔驰', price=500000},
// Car{name='宝马', price=600000},
// Car{name='保时捷', price=800000}]
}
}
/*
1. Car类(属性name,price)
2. 如果两个Car的name和price一样,则认为是相同元素,就不能添加
*/
class Car {
private String name;
private int price;
public Car(String name, int price) {
this.name = name;
this.price = price;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
@Override
public String toString() {
return "\nCar{" +
"name='" + name + '\'' +
", price=" + price +
'}';
}
//重写 equals 方法 和 hashCode
//当 name 和 price 相同时, 就返回相同的 hashCode 值, equals 返回 t
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Car car = (Car) o;
return price == car.price && Objects.equals(name, car.name);
}
@Override
public int hashCode() {
return Objects.hash(name, price);
}
}
11. Map 接口和常用方法
11.1 Map 接口实现类的特点 [很实用]
JDK8的Map接口特点:
- Map与Collection并列存在。用于保存具有映射关系的数据:Key-Value
- Map 中的 key 和value可以是任何引用类型的数据,会封装到HashMap$Node对象中
- Map 中的 key 不允许重复,原因和HashSet一样,前面分析过源码
- Map中的value 可以重复
- Map 的key 可以为 null,value也可以为null,注意key为null,只能有一个,value为null,可以多个.
- 常用String类作为Map的 key
- key 和value之间存在单向一对一关系,即通过指定的 key总能找到对应的value
public class Map_ {
public static void main(String[] args) {
/*
Map 接口实现类的特点, 使用实现类 HashMap
1. Map与Collection并列存在。用于保存具有映射关系的数据:Key-Value
2. Map 中的 key 和value可以是任何引用类型的数据,会封装到HashMap$Node对象中
3. Map 中的 key 不允许重复,原因和HashSet一样,前面分析过源码
4)Map中的value 可以重复
4. Map 的key 可以为 null,value也可以为null,注意key为null,只能有一个
5. value为null,可以多个.
6. 常用String类作为Map的 key
7. key 和value之间存在单向一对一关系,即通过指定的 key总能找到对应的value
*/
Map map = new HashMap();
map.put("N01", "孙悟空");//k-v
map.put("N02", "猪八戒");//k-v
map.put("N03", "沙和尚");//k-v
map.put("N04", "唐三藏");//k-v
map.put("N04", "白龙马");//当有相同的key,等价于替换
map.put(null, null); //k-v
map.put(null, "abc"); //等价替换
map.put("no5", null); //k-v
map.put("no6", null); //k-v
map.put(1, "三藏");
map.put(new Object(), "白骨精");
System.out.println(map);
//输出{
// N03=沙和尚,
// null=abc,
// N02=猪八戒,
// 1=三藏,
// N04=白龙马,
// no6=null,
// no5=null,
// java.lang.Object@1b6d3586=白骨精,
// N01=孙悟空}
//通过 get 方法,传入 key ,会返回对应的 value
System.out.println(map.get(1));//三藏
}
}
-
下图为Map存放数据的key-value示意图:
- 一对k-v是放在一个HashMap$Node中的,
- 又因为Node 实现了 Entry接口,
- 有些书上也说一对k-v就是一个Entry(如图)
11.2 Map 接口常用方法
- remove:根据键删除映射关系
- get:根据键获取值
- size:获取元素个数
- isEmpty:判断个数是否为 0
- clear:清除 k-v
- containsKey:查找键是否存在
- 下面进行简单的操作
public class MapMethod {
public static void main(String[] args) {
Map map = new HashMap();
map.put("孙悟空", new Book("", 1));//OK
map.put("孙悟空", "紫霞仙子");//替换上面的
map.put("牛魔王", "铁扇公主");//OK
map.put("猪八戒", "高老庄");//OK
map.put("沙和尚", "流沙河");//OK
map.put("白龙马", "唐三藏");//OK
System.out.println("map = " + map);
//输出map = {
// 白龙马=唐三藏,
// 牛魔王=铁扇公主,
// 沙和尚=流沙河,
// 孙悟空=紫霞仙子,
// 猪八戒=高老庄}
//1. remove:根据键删除映射关系
map.remove("白龙马");
System.out.println("newMap = " + map);
//输出newMap = {
// 牛魔王=铁扇公主,
// 沙和尚=流沙河,
// 孙悟空=紫霞仙子,
// 猪八戒=高老庄}
//2. get:根据键获取值
Object value = map.get("孙悟空");
System.out.println("value = " + value);
//输出 value = 紫霞仙子
//3. size:获取元素个数
int size = map.size();
System.out.println("k-v =" + size);
//输出 k-v =4
//4. isEmpty:判断个数是否为 0
System.out.println(map.isEmpty());//false
//5. clear:清除 k-v
map.clear();//全部清空
System.out.println("map=" + map);// map={}
//6. containsKey:查找键是否存在
boolean 白龙马 = map.containsKey("白龙马");
System.out.println("白龙马 = " + 白龙马);//白龙马 = false
}
}
class Book {
private String name;
private int num;
public Book(String name, int num) {
this.name = name;
this.num = num;
}
}
11.3 Map 接口遍历方法
map遍历比List和Set复杂一点,但是基本的原理是一样的
- containsKey:查找键是否存在
- keySet:获取所有的键
- values:获取所有的值
- entrySet:获取所有关系k-v
- 代码演示:
public class MapFor {
public static void main(String[] args) {
Map map = new HashMap();
map.put("孙悟空", "紫霞仙子");//OK
map.put("牛魔王", "铁扇公主");//OK
map.put("猪八戒", "高老庄");//OK
map.put("沙和尚", "流沙河");//OK
map.put("白龙马", "唐三藏");//OK
//第一组: 先取出 所有的 Key , 通过 Key 取出对应的 Value
Set set = map.keySet();
System.out.println("====第一种方式====");
//增强for
for (Object key : set) {
System.out.println(key + "_" + map.get(key));
//白龙马_唐三藏
//牛魔王_铁扇公主
//沙和尚_流沙河
//孙悟空_紫霞仙子
//猪八戒_高老庄
}
System.out.println("====第二种方式====");
//通过迭代器
Iterator iterator = set.iterator();
while (iterator.hasNext()) {
Object key = iterator.next();
System.out.println(key + "_" + map.get(key));
//白龙马_唐三藏
//牛魔王_铁扇公主
//沙和尚_流沙河
//孙悟空_紫霞仙子
//猪八戒_高老庄
}
//第二组: 把所有的 values 取出
System.out.println("====第三种方式====");
//增强for
Collection values = map.values();
for (Object value : values) {
System.out.println(value);
//唐三藏
//铁扇公主
//流沙河
//紫霞仙子
//高老庄
}
System.out.println("====第四种方式====");
//迭代器
Iterator iterator1 = values.iterator();
while (iterator1.hasNext()) {
Object value = iterator1.next();
System.out.println(value);
//唐三藏
//铁扇公主
//流沙河
//紫霞仙子
//高老庄
}
//第三组: 通过 EntrySet 来获取 k-v
System.out.println("====第五种方式====");
Set entrySet = map.entrySet();
//增强for
for (Object entry : entrySet) {
//将entry转为Map.entry
//向下转型
Map.Entry m = (Map.Entry) entry;
System.out.println(m.getKey() + "_" + m.getValue());
//白龙马_唐三藏
//牛魔王_铁扇公主
//沙和尚_流沙河
//孙悟空_紫霞仙子
//猪八戒_高老庄
}
System.out.println("====第六种方式====");
//迭代器
Iterator iterator2 = entrySet.iterator();
while (iterator2.hasNext()) {
Object entry = iterator2.next();
//将entry转为Map.entry
//向下转型
Map.Entry m = (Map.Entry) entry;
System.out.println(m.getKey() + "_" + m.getValue());
//白龙马_唐三藏
//牛魔王_铁扇公主
//沙和尚_流沙河
//孙悟空_紫霞仙子
//猪八戒_高老庄
}
}
}
11.4 Map 接口练习
- 使用HashMap添加三个员工对象
- 要求:
- 键:员工id
- 值:员工对象
- 并遍历显示工资>18000的员工(遍历方式至少两种)
- 员工类:姓名、工资、员工id
public class MapExercise {
public static void main(String[] args) {
Map hashMap = new HashMap();
hashMap.put(126, new Employee("张三", 12000, 126));
hashMap.put(128, new Employee("李白", 19000, 128));
hashMap.put(132, new Employee("王琳", 21000, 132));
//第一种:keySet -> 增强for
System.out.println("====增强for遍历====");
Set set = hashMap.keySet();
for (Object key : set) {
//先获取value
Employee employee = (Employee) hashMap.get(key);
if (employee.getSal() > 18000) {
System.out.println(employee);
}
}
//第二种:entrySet -> 迭代器
System.out.println("====迭代器遍历====");
Set entry = hashMap.entrySet();
Iterator iterator = entry.iterator();
while (iterator.hasNext()) {
Map.Entry m = (Map.Entry) iterator.next();
//通过 entry 取得 key 和 value
Employee employee = (Employee) m.getValue();
if (employee.getSal() > 18000) {
System.out.println(employee);
}
}
}
}
/*
1. 使用HashMap添加三个员工对象
2. 要求:
- 键:员工id
- 值:员工对象
3. 并遍历显示工资>18000的员工(遍历方式至少两种)
4. 员工类:姓名、工资、员工id
*/
class Employee {
private String name;
private double sal;
private int id;
public Employee(String name, double sal, int id) {
this.name = name;
this.sal = sal;
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getSal() {
return sal;
}
public void setSal(double sal) {
this.sal = sal;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
@Override
public String toString() {
return "Employee{" +
"name='" + name + '\'' +
", sal=" + sal +
", id=" + id +
'}';
}
}
12. Map 接口实现类-HashMap*
12.1 HashMap 小结
- Map接口的常用实现类:HashMap、Hashtable和Properties。
- HashMap是Map接口使用频率最高的实现类。
- HashMap 是以 key-val对的方式来存储数据(HashMap$Node类型)[案例 Entry]
- key不能重复,但是值可以重复,允许使用null键和null值。
- 如果添加相同的key,则会覆盖原来的key-val,等同于修改(kev不会替换,val会替换)
- 与HashSet一样,不保证映射的顺序,因为底层是以hash表的方式来存储的。(idk8的hashMap底层 数组+链表+红黑树)
- HashMap没有实现同步,因此是线程不安全的,方法没有做同步互斥的操作,没有synchronized
12.2 HashMap 底层机制及源码剖析
1. 扩容机制[和HashSet相同:
- HashMap底层维护了Node类型的数组table,默认为null
- 当创建对象时,将加载因子(loadfactor)初始化为0.75.
- 当添加key-val 时,通过kev的哈希值得到在table的索引。然后判断该索引处是否有元素:
- 如果没有元素直接添加。
- 如果该索引处有元素,继续判断该元素的key和准备加入的key是否相等:
- 如果相等,则直接替换val;
- 如果不相等需要判断是树结构还是链表结构,做出相应处理。
- 如果添加时发现容量不够,则需要扩容。
- 第1次添加,则需要扩容table容量为16,临界值(threshold)为12(16*0.75
- 以后再扩容,则需要扩容table容量为原来的2倍(32),临界值为原来的2倍,即24.依次类推
- 在Java8中,如果一条链表的元素个数超过TREEIFYTHRESHOLD(默认是8),并且table的大小 >=MIN TREEIFY CAPACITY(默认64),就会进行树化(红黑树)
- 看如下示意图
- 下面进行代码分析:
public class HashMapSource1 {
public static void main(String[] args) {
HashMap map = new HashMap();
map.put("java", 10);//ok
map.put("php", 10);//ok
map.put("java", 20);//替换 value
System.out.println("map=" + map);//
/*源码解读 HashMap 的源码
1.执行构造器 new HashMap()
初始化加载因子 loadfactor = 0.75
HashMap$Node[] table = null
2.执行 put 调用 hash 方法,计算 key 的 hash 值 (h = key.hashCode()) ^ (h >>> 16)
public V put(K key, V value) {//K = "java" value = 10
return putVal(hash(key), key, value, false, true);
}
3.执行 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;
//取出 hash 值对应的 table 的索引位置的 Node, 如果为 null, 就直接把加入的 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;//辅助变量
// 如果 table 的索引位置的 key 的 hash 相同和新的 key 的 hash 值相同,
// 并 满足(table 现有的结点的 key 和准备添加的 key 是同一个对象 || equals 返回真)
// 就认为不能加入新的 k-v
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)//如果当前的 table 的已有的 Node 是红黑树,就按照红黑树的方式处理
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else {
//如果找到的结点,后面是链表,就循环比较
for (int binCount = 0; ; ++binCount) {//死循环
if ((e = p.next) == null) {//如果整个链表,没有和他相同,就加到该链表的最后
p.next = newNode(hash, key, value, null);
//加入后,判断当前链表的个数,是否已经到 8 个,到 8 个,后
//就调用 treeifyBin 方法进行红黑树的转换
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash && //如果在循环比较过程中,发现有相同,就 break,就只是替换 value
((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[12-24-48])//如 size > 临界值,就扩容
resize();
afterNodeInsertion(evict);
return null;
}
5. 关于树化(转成红黑树)
//如果 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();
}
*/
}
}
2. 模拟HashMap触发扩容、树化情况,并Debug验证
public class HashMapSource2 {
public static void main(String[] args) {
HashMap hashMap = new HashMap();
for (int i = 1; i <= 12; i++) {
hashMap.put(i, "hello");
}
hashMap.put("aaa", "bbb");
System.out.println("hashMap=" + hashMap);//12 个 k-v
//自己设计代码去验证,table 的扩容
//0 -> 16(12) -> 32(24) -> 64(64*0.75=48)-> 128 (96) ->
}
}
class A {
private int num;
public A(int num) {
this.num = num;
}
//所有的 A 对象的 hashCode 都是 100
// @Override
// public int hashCode() {
// return 100;
// }
@Override
public String toString() {
return "\nA{" +
"num=" + num + '}';
}
}
13. Map 接口实现类-Hashtable
13.1 HashTable 的基本介绍
- 存放的元素是键值对:即 K-V
- hashtable的键和值都不能为null,否则会抛出NullPointerException
- hashTable 使用方法基本上和HashMap一样
- hashTable是线程安全的(synchronized),hashMap是线程不安全的
- 简单看下底层结构 HashTable的应用案例
- 下面的代码是否正确,如果错误,为什么?
HashTableExercise.java Hashtable table=new Hashtable)://ok
table.put("john", 100); //ok
table.put(null,100);//异常
table.put("john", null);//异常
table.put("lucy", 100);//ok
table.put("lic" 100);//ok
table.put("lic"88);//替换
System.out.printIn(table);
13.2 Hashtable 和 HashMap 对比
14. Map 接口实现类-Properties
14.1 基本介绍
-
Properties类继承自Hashtable类并且实现了Map接口,也是使用一种键值对的形式来保存数据。
-
他的使用特点和Hashtable类似
-
Properties 还可以用于从 xxx.properties文件中,加载数据到Properties类对象并进行读取和修改
-
说明:工作后xxx.properties文件通常作为配置文件,这个知识点在1O流举例,有兴趣可先看文章
https://www.cnblogs.com/xudong-bupt/p/3758136.html
14.2 基本使用
public class Properties_ {
public static void main(String[] args) {
//代码解读
//1. Properties 继承 Hashtable
//2. 可以通过 k-v 存放数据,当然 key 和 value 不能为 null
//增加
Properties properties = new Properties();
//properties.put(null, "abc");//抛出 空指针异常
//properties.put("abc", null); //抛出 空指针异常
properties.put("john", 100);//k-v
properties.put("lucy", 100);
properties.put("lic", 100);
properties.put("lic", 88);//如果有相同的 key , value 被替换
System.out.println("properties=" + properties);
//通过 k 获取对应值
System.out.println(properties.get("lic"));//88
//删除 properties.remove("lic");
System.out.println("properties=" + properties);
//修改
properties.put("john", "约翰");
System.out.println("properties=" + properties);
}
}
15. 总结-集合选型规则(重点记住)
在开发中,选择什么集合实现类,主要取决于业务操作特点,然后根据集合实现类特性进行选择,分析如下:
-
先判断存储的类型(一组对象[单列]或一组键值对[双列])
-
一组对象[单列]:Collection接口
- 允许重复:List
- 增删多:LinkedList[底层维护了一个双向链表]
- 改查多:ArrayList[底层维护 Object类型的可变数组
- 不允许重复:Set
- 无序:HashSet「底层是HashMap,维护了一个哈希表即(数组+链表+红黑树)
- 排序:TreeSet [下面会举例说明]
- 插入和取出顺序一致:LinkedHashSet,维护数组+双向链表
- 允许重复:List
-
一组键值对[双列]:Map
- 键无序:HashMap[底层是:哈希表jdk7:数组+链表,jdk8:数组+链表+红黑树]
- 键排序:TreeMap [下面会举例说明]
- 键插入和取出顺序一致:LinkedHashMap
- 读取文件 Properties
16. TreeSet与TreeMap
TreeSet源码解读
public class TreeSet_ {
public static void main(String[] args) {
//1. 当我们使用无参构造器,创建 TreeSet 时,仍然是无序的
//2. 现在希望添加的元素,按照字符串大小来排序
//3. 使用 TreeSet 提供的一个构造器,可以传入一个比较器(匿名内部类)
// 并指定排序规则
//4. 下面简单阅读一下源码
/*
1.构造器把传入的比较器对象,赋给了 TreeSet 的底层的 TreeMap 的属性 this.comparator
public TreeMap(Comparator<? super K> comparator)
{ this.comparator = comparator;
}
2.在 调用 treeSet.add("tom"), 在底层会执行到
if (cpr != null) {//cpr 就是我们的匿名内部类(对象)
do {
parent = t;
//动态绑定到我们的匿名内部类(对象)compare cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left; else if (cmp > 0)
t = t.right;
else //如果相等,即返回 0,这个 Key 就没有加入
return t.setValue(value);
} while (t != null);
}
*/
//TreeSet treeSet = new TreeSet();
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();
}
});
//添加数据. treeSet.add("jack");
treeSet.add("tom");//3
treeSet.add("sp");
treeSet.add("a");
treeSet.add("abc");//3
System.out.println("treeSet=" + treeSet);
}
}
TreeMap源码阅读
public class TreeMap_ {
public static void main(String[] args) {
//使用默认的构造器,创建 TreeMap, 是无序的(也没有排序)
/*
要求:按照传入的 k(String) 的大小进行排序
*/
// TreeMap treeMap = new TreeMap();
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();
}
});
treeMap.put("jack", "杰克");
treeMap.put("tom", "汤姆");
treeMap.put("kristina", "克瑞斯提诺");
treeMap.put("smith", "斯密斯");
treeMap.put("hsp", "韩顺平");//加入不了
System.out.println("treemap=" + treeMap);
/*
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(key, key); // type (and possibly null) check
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
2.2以后添加
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);
}
*/
}
}
17. Collections 工具类
17.1 Collections 工具类介绍
- Collections是一个操作 SetList和Map等集合的工具类
- Collections中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作
17.2 排序操作:(均为static 方法)
- reverse(List):反转 List中元素的顺序
- shuffle(List):对List集合元素进行随机排序
- sort(List):根据元素的自然顺序对指定 List集合元素按升序排序
- sort(List,Comparator):根据指定的 Comparator产生的顺序对List集合元素进行排序
- swap(List,int,int):将指定list集合中的处元素和j处元素进行交换
17.3 查找、替换
- Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
- Object max(Collection,Comparator):根据Comparator指定的顺序,
返回给定集合中的最大元素 - Object min(Collection)
- Object min(Collection,Comparator)
- int frequency(Collection,Object):返回指定集合中指定元素的出现次数
- void copy(List destListsrc):将src中的内容复制到dest中
- boolean replaceAll(List list,Object oldValObject newVal):使用新值替换 List 对象的所有旧值
17.4 Collections 案例演示
public class Collections_ {
public static void main(String[] args) {
//创建 ArrayList 集合,用于测试.
List list = new ArrayList();
list.add("tom");
list.add("smith");
list.add("king");
list.add("milan");
list.add("tom");
//reverse(List):反转 List 中元素的顺序
Collections.reverse(list);
System.out.println("list=" + list);
//shuffle(List):对 List 集合元素进行随机排序
// for (int i = 0; i < 5; i++) {
// Collections.shuffle(list);
// System.out.println("list=" + list);
// }
//sort(List):根据元素的自然顺序对指定 List 集合元素按升序排序
Collections.sort(list);
System.out.println("自然排序后");
System.out.println("list=" + list);
//sort(List,Comparator):根据指定的 Comparator 产生的顺序对 List 集合元素进行排序
//我们希望按照 字符串的长度大小排序
Collections.sort(list, new Comparator() {
@Override
public int compare(Object o1, Object o2) {
//可以加入校验代码.
return ((String) o2).length() - ((String) o1).length();
}
});
System.out.println("字符串长度大小排序=" + list);
// swap(List,int, int):将指定 list 集合中的 i 处元素和 j 处元素进行交换
//比如 Collections.swap(list, 0, 1);
System.out.println("交换后的情况");
System.out.println("list=" + list);
//Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
System.out.println("自然顺序最大元素=" + Collections.max(list));
//Object max(Collection,Comparator):根据 Comparator 指定的顺序,返回给定集合中的最大元素
//比如,我们要返回长度最大的元素
Object maxObject = Collections.max(list, new Comparator() {
@Override
public int compare(Object o1, Object o2) {
return ((String) o1).length() - ((String) o2).length();
}
});
System.out.println("长度最大的元素=" + maxObject);
//Object min(Collection)
//Object min(Collection,Comparator)
//上面的两个方法,参考 max 即可
//int frequency(Collection,Object):返回指定集合中指定元素的出现次数
System.out.println("tom 出现的次数=" + Collections.frequency(list, "tom"));
//void copy(List dest,List src):将 src 中的内容复制到 dest 中
ArrayList dest = new ArrayList();
//为了完成一个完整拷贝,我们需要先给 dest 赋值,大小和 list.size()一样
for (int i = 0; i < list.size(); i++) {
dest.add("");
}
//拷贝
Collections.copy(dest, list);
System.out.println("dest=" + dest);
//boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换 List 对象的所有旧值
//如果 list 中,有 tom 就替换成 汤姆
Collections.replaceAll(list, "tom", "汤姆");
System.out.println("list 替换后=" + list);
}
}
18. 集合练习
1. 练习一
- 封装一个新闻类,包含标题和内容属性,提供get、set方法,重写toString方法,打印对象时只打印标题:
- 只提供一个带参数的构造器,实例化对象时,只初始化标题;并且实例化两个对象:
- 新闻一:新冠确诊病例超千万,数百万印度教信徒赴恒河“圣浴”引民众担忧
- 新闻二:男子突然想起2个月前钓的鱼还在网兜里,捞起一看赶紧放生
- 将新闻对象添加到ArrayList集合中,并且进行倒序遍历;
- 在遍历集合过程中,对新闻标题进行处理,超过15字的只保留前15个,然后在后边加
- 在控制台打印遍历出经过处理的新闻标题;
- 代码编辑:
public class Homework01 {
public static void main(String[] args) {
ArrayList arrayList = new ArrayList();
arrayList.add(new News("新冠确诊病例超千万,数百万印度教信徒赴恒河\"圣浴\"引民众担忧"));
arrayList.add(new News("男子突然想起2个月前钓的鱼还在网兜里,捞起一看赶紧放生"));
//取出新闻长度
int size = arrayList.size();
//倒序遍历
for (int i = size - 1; i >= 0; i--) {
//System.out.println(arrayList.get(i));
News news = (News) arrayList.get(i);
//直接调用
System.out.println(processTitle(news.getTitle()));
//男子突然想起2个月前钓的鱼还在...
//新冠确诊病例超千万,数百万印度...
}
}
//专门写一个方法,处理实现新闻标题
//processTitle 处理
//加个 static 静态方法 ,可以直接调用
public static String processTitle(String title) {
if (title == null) {
return "";
}
if (title.length() > 15) {
//截取0-15 [0,15)
return title.substring(0, 15) + "...";
} else {
return title;
}
}
}
/*
1. 封装一个新闻类,包含标题和内容属性,提供get、set方法,重写toString方法,打印对象时只打印标题:
2. 只提供一个带参数的构造器,实例化对象时,只初始化标题;并且实例化两个对象:
新闻一:新冠确诊病例超千万,数百万印度教信徒赴恒河“圣浴”引民众担忧
新闻二:男子突然想起2个月前钓的鱼还在网兜里,捞起一看赶紧放生
3. 将新闻对象添加到ArrayList集合中,并且进行倒序遍历;
4. 在遍历集合过程中,对新闻标题进行处理,超过15字的只保留前15个,然后在后边加
5. 在控制台打印遍历出经过处理的新闻标题;
*/
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;
}
//重写toString方法,打印对象时只打印标题:
@Override
public String toString() {
return "News{" +
"title='" + title + '\'' +
'}';
}
}
2. 练习二
- 使用ArrayList 完成对 对象 Car {name,price}的各种操作
- 要求
- add:添加单个元素
- remove:删除指定元素
- contains:查找元素是否存在
- size:获取元素个数
- isEmpty:判断是否为空
- clear:清空
- addAll:添加多个元素
- containsAll:查找多个元素是否都存在
- removeAll:删除多个元素
- 使用增强for和 迭代器来遍历所有的car,需要重写 Car 的toString方法
- Car car=new Car("宝马400000);
- Car car2 =new Car("宾利"5000000);
- 代码如下:
public class Homework02 {
public static void main(String[] args) {
ArrayList arrayList = new ArrayList();
Car car = new Car("宝马", 400000);
Car car2 = new Car("宾利", 5000000);
//- add:添加单个元素
arrayList.add(car);
arrayList.add(car2);
System.out.println("arrayList =" + arrayList);
//输出arrayList =[Car{name='宝马', price=400000}, Car{name='宾利', price=5000000}]
//- remove:删除指定元素
arrayList.remove(1);
System.out.println("arrayList =" + arrayList);
//输出arrayList =[Car{name='宝马', price=400000}]
//- contains:查找元素是否存在
System.out.println(arrayList.contains(car));//true
//- size:获取元素个数
System.out.println(arrayList.size());//1
//- isEmpty:判断是否为空
System.out.println(arrayList.isEmpty());//false
//- clear:清空
//arrayList.clear();
//- addAll:添加多个元素
boolean b = arrayList.addAll(arrayList);
System.out.println(b);//true
System.out.println("arrayList =" + arrayList);
//输出arrayList =[Car{name='宝马', price=400000}, Car{name='宝马', price=400000}]
//- containsAll:查找多个元素是否都存在
boolean bom = arrayList.containsAll(arrayList);
System.out.println(bom);//true
//- removeAll:删除多个元素
//arrayList.removeAll(arrayList);//相当于清空
//- 使用增强for和 迭代器来遍历所有的car,需要重写 Car 的toString方法
//增强for
System.out.println("====增强for====");
for (Object o : arrayList) {
System.out.println(o);
//Car{name='宝马', price=400000}
//Car{name='宝马', price=400000}
}
//迭代器
System.out.println("====迭代器====");
Iterator iterator = arrayList.iterator();
while (iterator.hasNext()) {
Object next = iterator.next();
System.out.println(next);
//Car{name='宝马', price=400000}
//Car{name='宝马', price=400000}
}
}
}
class Car {
private String name;
private int price;
public Car(String name, int price) {
this.name = name;
this.price = price;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
@Override
public String toString() {
return "Car{" +
"name='" + name + '\'' +
", price=" + price +
'}';
}
}
3. 练习三
- 使用HashMap类实例化一个Map类型的对象m,键(String)和值(int)分别用于存储员工的姓名和工资,
- 存入数据如下:
- jack-650元;
- tom-1200元;
- smith–2900元;
- 将iack的工资更改为2600元
- 为所有员工工资加薪100元;
- 遍历集合中所有的员工
- 遍历集合中所有的工资
- 代码如下:
public class Homework03 {
public static void main(String[] args) {
Map m = new HashMap();
m.put("jack", 650);
m.put("tom", 1200);
m.put("smith", 2900);
System.out.println(m);
//{tom=1200, smith=2900, jack=650}
m.put("jack", 2600);
System.out.println(m);
//{tom=1200, smith=2900, jack=2600}
Set set = m.keySet();
for (Object key : set) {
m.put(key, (Integer) m.get(key) + 100);
}
System.out.println(m);
//{tom=1300, smith=3000, jack=2700}
//遍历集合中所有的员工
//遍历 EntrySet
Set entrySet = m.entrySet();
//迭代器
Iterator iterator = entrySet.iterator();
while (iterator.hasNext()) {
Map.Entry entry = (Map.Entry) iterator.next();
System.out.println(entry.getKey() + "_" + entry.getValue());
//tom_1300
//smith_3000
//jack_2700
}
//遍历集合中所有的工资
//取出value
System.out.println("遍历所有工资");
Collection values = m.values();
for (Object value : values) {
System.out.println("工资 =" + value);
//工资 =1300
//工资 =3000
//工资 =2700
}
}
}
/*
1. 使用HashMap类实例化一个Map类型的对象m,键(String)和值(int)分别用于存储员工的姓名和工资,
2. 存入数据如下:
- jack-650元;
- tom-1200元;
- smith--2900元;
3. 将iack的工资更改为2600元
4. 为所有员工工资加薪100元;
5. 遍历集合中所有的员工
6. 遍历集合中所有的工资
*/
class Employee {
private String name;
private int sal;
public Employee(String name, int sal) {
this.name = name;
this.sal = sal;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getSal() {
return sal;
}
public void setSal(int sal) {
this.sal = sal;
}
@Override
public String toString() {
return "Employee{" +
"name='" + name + '\'' +
", sal=" + sal +
'}';
}
}
4. 练习四
- 试分析HashSet和TreeSet分别如何实现去重的
- HashSet的去重机制:
- hashCode()+equals(),
- 底层先通过存入对象,进行运算得到一个 hash值,通过hash值得到对应的索引,
- 如果发现table索引所在的位置,没有数据,就直接存放
- 如果有数据,就进行equals比较[遍历比较],如果比较后,不相同,就加入,否则就不加入.
- TreeSet的去重机制:
- 如果你传入了一个Comparator匿名对象,就使用实现的compare去重,
- 如果方法返回0,就认为是相同的元素/数据,就不添加,
- 如果你没有传入一个Comparator匿名对象,则以你添加的对象实现的Compareable接口的compareTo去重
5. 练习五
-
下面代码运行会不会抛出异常,并从源码层面说明原因。[考察 读源码+接口编程+动态绑定]
TreeSet treeSet =new TreeSet(); treeSet.add(new Person());
-
代码分析
public class Homework05 { public static void main(String[] args) { TreeSet treeSet = new TreeSet(); //分析 //add方法,因为 TreeSet() 构造器没有传入Comparator接口的匿名内部类 //所以在底层 Comparable<? super K> k = (Comparable<? super K>) key; //即 把 Person 转成 Comparable 类型 treeSet.add(new Person()); //ClassCastException //类型转换异常 } } class Person {} //让 Person 类实现 Comparable就不会报错 /* class Person implements Comparable{ @Override public int compareTo(Object o) { return 0; } } */
6. 练习六
- 已知:Person类按照id和name重写了hashCode和equals方法,
- 问下面代码输出什么?
HashSet set = new HashSet()://ok
Person p1 = new Person(1001,"AA")//ok
Person p2 =new Person(1002,"BB");//ok
set.add(p1)://ok
set.add(p2);//ok
p1.name ="CC";
set.remove(p1);
System.out.printin(set);//2
set.add(new Person(1001,"CC"));
System.out.println(set);//3
set.add(new Person(1001,"AA"));
System.out.println(set);//4
- 代码如下:
public class Homework06 {
public static void main(String[] args) {
HashSet set = new HashSet();//ok
Person p1 = new Person(1001, "AA");//ok
Person p2 = new Person(1002, "BB");//ok
set.add(p1);//ok
set.add(p2);//ok
p1.name = "CC";
set.remove(p1);
System.out.println(set);
//输出 [Person{id=1002, name='BB'},
// Person{id=1001, name='CC'}]
set.add(new Person(1001, "CC"));
System.out.println(set);
//输出 [Person{id=1002, name='BB'},
// Person{id=1001, name='CC'},
// Person{id=1001, name='CC'}]
set.add(new Person(1001, "AA"));
System.out.println(set);
//输出 [Person{id=1002, name='BB'},
// Person{id=1001, name='CC'},
// Person{id=1001, name='CC'},
// Person{id=1001, name='AA'}]
}
}
class Person {
private int id;
public String name;
public Person(int id, String name) {
this.id = id;
this.name = name;
}
public int getNum() {
return id;
}
public void setNum(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Person{" +
"id=" + id +
", 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);
}
}
7. 练习七
- 试写出Vector和ArrayList的比较?
- 从底层结构进行比较:
- ArrayList是可变数组
- Vector是可变数组 Object[]
- 从版本进行比较
- ArrayList是 jdk1.2 诞生的
- Vector是 jdk1.0 诞生的
- 从线程安全(同步)效率进行比较
- ArrayList是不安全,但是效率高
- Vector是安全,但是效率不高
- 从扩容倍数进行比较
- ArrayList:
- 如果使用有参构造器按照1.5倍扩容;
- 如果是无参构造器
- 1.第一次扩容10
- 2.从第二次开始按照1.5倍扩容
- Vector:
- 如果是无参,默认10,满后,按照2倍扩容
- 如果是指定大小创建Vector,则每次按照2倍扩容.