集合
集合概述
- 概念:对象的容器,定义了对多个对象进行操作的常用方法。可实现数组的功能。
- 和数组的区别:
- 数组长度固定,集合长度不固定。
- 数组可以存储基本类型和引用类型,集合只能存储引用类型。如果使用集合存储基本类型,必须将基本类型装箱为对应的引用类型
- 位置: java.util.*;
Collection体系集合
Collection父接口
-
特点:代表一组任意类型的对象,无序、无下标、不能重复。
-
方法:
boolean add(Object obj) //添加一个对象。
boolean addAll(Collection c) //讲一个集合中的所有对象添加到此集合中。
void clear() //清空此集合中的所有对象。
boolean contains(Object o) //检查此集合中是否包含o对象。
boolean equals(Object o) //比较此集合是否与指定对象相等。
boolean isEmpty() //判断此集合是否为空。
boolean remove(Object o) //在此集合中移除o对象。
int size() //返回此集合中的元素个数。
Object[] toArray() //将此集合转换成数组。
/** * Collection接口的使用(一) * 1.添加元素 * 2.删除元素 * 3.遍历元素 * 4.判断 */ public static void main(String[] args){ //创建集合 Collection collection=new ArrayList(); //接口回调 // * 1.添加元素 collection.add("苹果"); collection.add("西瓜"); collection.add("榴莲"); System.out.println("元素个数:"+collection.size()); System.out.println(collection);//[苹果, 西瓜, 榴莲] // * 2.删除元素 collection.remove("榴莲"); System.out.println("删除之后:"+collection.size()); // * 3.遍历元素 //3.1 使用增强for for(Object object : collection){ // ArrayList 返回的是Object类型 System.out.println(object); } //3.2 使用迭代器(迭代器专门用来遍历集合的一种方式) //hasnext();判断是否有下一个元素 //next();获取下一个元素 读取的也是Object类型 //remove();移除迭代器返回的最后一个元素 //it.next();it.next();it.next();remove()会删除最后一个也就是第三个返回的元素 Iterator it=collection.iterator(); while(it.hasNext()){ String s=(String)it.next(); System.out.println(s); //删除操作 //collection.remove(s); 使用此方法会引发并发修改异常 //it.remove(); 使用迭代器的方法,移除迭代器返回的最后一个元素 } // * 4.判断 System.out.println(collection.contains("西瓜"));//true System.out.println(collection.isEmpty());//false }
/** * Collection接口的使用(二) * 1.添加元素 * 2.删除元素 * 3.遍历元素 * 4.判断 */ public class Demo2 { public static void main(String[] args) { Collection collection=new ArrayList(); Student s1=new Student("张三",18); Student s2=new Student("李四", 20); Student s3=new Student("王五", 19); //1.添加数据 collection.add(s1); collection.add(s2); collection.add(s3); //collection.add(s3);可重复添加相同对象,因为new ArrayList(); System.out.println("元素个数:"+collection.size()); System.out.println(collection.toString());// 此处toString()方法调用的是Student类重 //2.删除数据 //写的方法 collection.remove(s1); System.out.println("删除之后:"+collection.size()); //3.遍历数据 //3.1 增强for for(Object object:collection) { Student student=(Student) object; System.out.println(student.toString()); } //3.2迭代器 //迭代过程中不能使用collection的删除方法 //next()返回的是Object类型 Iterator iterator=collection.iterator(); while (iterator.hasNext()) { System.out.println(iterator.next().toString());//上转型对象 } //4.判断和上一块代码类似。 } }
/** * 学生类 */ public class Student { private String name; private int age; public Student(String name, int age) { super(); 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 "Student [name=" + name + ", age=" + age +"]"; } }
Collection子接口
List集合
-
特点:有序(添加的顺序和遍历获取的顺序是一致的)、有下标、元素可以重复。
-
方法:
void add(int index,Object o) //在index位置插入对象o。
boolean addAll(index,Collection c) //将一个集合中的元素添加到此集合中的index位置。
Object get(int index) //返回集合中指定位置的元素。
List subList(int fromIndex,int toIndex) //返回fromIndex和toIndex之间的集合元素。
/** * List子接口的使用(一) * 1.添加元素 * 2.删除元素 * 3.遍历元素 * 4.判断 * 5.获取位置 */ public static void main(String[] args) { List list=new ArrayList(); //1.添加元素 list.add("chen"); list.add("shu"); list.add(0,"bo");// 插入操作 System.out.println("元素个数:"+list.size()); System.out.println(list.toString());//ArrayList继承的AbstractCollection类的方法 //2.删除元素 list.remove(0); // list.remove("bo");结果相同 System.out.println("删除之后:"+list.size()); System.out.println(list.toString()); //3.遍历元素 //3.1 使用for遍历 for(int i=0;i<list.size();i++) { System.out.println(list.get(i)); } //3.2 使用增强for for(Object object:list) { System.out.println(object); } //3.3 使用迭代器 Iterator iterator=list.iterator(); while (iterator.hasNext()) { System.out.println(iterator.next()); } //3.4使用列表迭代器,listIterator可以双向遍历,添加、删除及修改元素。 ListIterator lt=list.listIterator(); //从前往后 while (lt.hasNext()) { System.out.println(lt.nextIndex()+":"+lt.next()); } //从后往前(此时“遍历指针”已经指向末尾元素的后面) while(lt.hasPrevious()) { System.out.println(lt.previousIndex()+":"+lt.previous()); } //4.判断 System.out.println(list.isEmpty()); System.out.println(list.contains("tang")); //5.获取位置 System.out.println(list.indexOf("shu")); }
/** * List子接口的使用(二) * 1.添加元素 * 2.删除元素 * 3.遍历元素 * 4.判断 * 5.获取位置 */ public static void main(String[] args) { List list=new ArrayList(); //1.添加数字数据(隐含自动装箱Integer) list.add(20); list.add(30); list.add(40); list.add(50); System.out.println("元素个数:"+list.size()); System.out.println(list.toString()); //2.删除元素 //list.remove(0);下标为0 //list.remove(20);很明显数组越界错误,程序识别为移除下标为20的元素, //list.remove((Object)20); 正确 list.remove(new Integer(20)); //正确 System.out.println("元素个数:"+list.size()); System.out.println(list.toString()); //3-5不再演示,与之前类似 //6.方法subList,返回子集合,[1,3); List list2=list.subList(1, 3); System.out.println(list2.toString()); }
List实现类
ArrayList
- 查找遍历速度快,增删慢;
- 存储结构:数组
- 运行效率快、线程不安全。
/**
* ArrayList的使用
* 1.添加元素
* 2.删除元素
* 3.遍历元素
* 4.判断
* 5.查找
*/
public static void main(String[] args) {
ArrayList arrayList=new ArrayList();
//1.添加元素
Student s1=new Student("唐", 21);
Student s2=new Student("何", 22);
Student s3=new Student("余", 21);
arrayList.add(s1);
arrayList.add(s2);
arrayList.add(s3);
System.out.println("元素个数:"+arrayList.size());
System.out.println(arrayList.toString());
//2.删除元素
arrayList.remove(s1);
//arrayList.remove(new Student("唐", 21));
//不可以这样删除,属性相同但是显然这是两个不同的对象,堆中地址不同。
//3.遍历元素
//3.1使用迭代器
Iterator iterator=arrayList.iterator();
while(iterator.hasNext()) {
System.out.println(iterator.next());
}
//3.2使用列表迭代器
ListIterator listIterator=arrayList.listIterator();
//从前往后遍历
while(listIterator.hasNext()) {
System.out.println(listIterator.next());
}
//从后往前遍历
while(listIterator.hasPrevious()) {
System.out.println(listIterator.previous());
}
//4.判断
System.out.println(arrayList.isEmpty());
//System.out.println(arrayList.contains(new Student("何", 22)));
//属性相同但是显然这是两个不同的对象,堆中地址不同
//5.查找
System.out.println(arrayList.indexOf(s1));
}
ArrayList源码分析
-
默认容量大小:
private static final int DEFAULT_CAPACITY = 10;
-
存放元素的数组:
transient Object[] elementData;
-
实际元素个数:
private int size;
-
创建对象时调用的无参构造函数:
// 空数组 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }
这段源码说明当你没有向集合中添加任何元素时,集合容量为0。那么默认的10个容量怎么来的呢?
这就得康康add方法的源码了:
public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; }
new一个ArrayList之后,当时容量为0,size也为0。这时调用add方法进入到
ensureCapacityInternal(size + 1);
该方法源码如下:private void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); }
该方法中的参数minCapacity传入的值为size+1也就是 1,接着我们再进入到
calculateCapacity(elementData, minCapacity)
里面:private static int calculateCapacity(Object[] elementData, int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { return Math.max(DEFAULT_CAPACITY, minCapacity); } return minCapacity; }
if条件成立,返回10。这个值作为参数又传入
ensureExplicitCapacity()
方法中,进入该方法查看源码:private void ensureExplicitCapacity(int minCapacity) { modCount++; // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity); }
先不管modCount(修改次数)这个变量。
因为elementData数组长度为0,所以if条件成立,调用grow方法,重要的部分来了,我们进入到grow方法的源码中:
private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1);// 右移一位相当于除2 if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity); }
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; // Integer.MAX_VALUE 是int的最大值
Arrays.copyOf()
方法返回的数组是新的数组对象,原数组对象不会改变,该方法不会影响原来的数组。Arrays.copyOf()
的第二个自变量指定要建立的新数组长度,如果新数组的长度超过原数组的长度,则保留数组默认值。
Vector
-
查找遍历速度快、增删慢;
-
存储结构:数组
-
实现可增长的对象数组
-
运行效率慢、线程安全。
/** * Vector的演示使用 *1.添加数据 *2.删除数据 *3.遍历 *4.判断 */ public static void main(String[] args) { Vector vec=new Vector(); //1.添加数据 vec.add("chen"); vec.add("shu"); vec.add("bo"); System.out.println("元素个数:"+vec.size()); //2.删除数据 // vec.remove(0); 下标索引 // vec.remove("chen"); Object类型 //3.遍历 //使用枚举器 Enumeration enumeration=vec.elements(); while (enumeration.hasMoreElements()) { String s = (String) enumeration.nextElement(); // 返回Object类型 System.out.println(s); } //4.判断 System.out.println(vec.isEmpty()); System.out.println(vec.contains("bo")); //5. Vector其他方法 vec.firstElement(); // 第一个元素 vec.lastElement(); // 最后一个元素 vec.elementAt(1); // 指定 1 下标的元素 }
LinkedList
- 增删快,查找遍历慢。
- 存储结构:双向链表
/**
* LinkedList的用法
* 1.添加元素
* 2.删除元素
* 3.遍历
* 4.判断
*/
public static void main(String[] args) {
LinkedList linkedList=new LinkedList();
Student s1=new Student("唐", 21);
Student s2=new Student("何", 22);
Student s3=new Student("余", 21);
//1.添加元素
linkedList.add(s1);
linkedList.add(s2);
linkedList.add(s3);
linkedList.add(s3);
System.out.println("元素个数:"+linkedList.size());
System.out.println(linkedList.toString());
//2.删除元素
// linkedList.remove(s1); // Object类型
// linkedList.remove(1); // 下标索引
//3.遍历
//3.1 使用for
for(int i=0;i<linkedList.size();i++) {
System.out.println(linkedList.get(i));
}
//3.2 使用增强for
for(Object object:linkedList) {
Student student=(Student) object;
System.out.println(student.toString());
}
//3.3 使用迭代器
Iterator iterator =linkedList.iterator();
while (iterator.hasNext()) {
Student student = (Student) iterator.next();
System.out.println(student.toString());
}
//3.4 使用列表迭代器
//4. 判断
System.out.println(linkedList.contains(s1));
System.out.println(linkedList.isEmpty());
System.out.println(linkedList.indexOf(s3));
}
LinkedList源码分析
LinkedList有三个属性:
- 链表大小:
transient int size = 0;
- (指向)第一个结点/头结点:
transient Node<E> first;
// 初始为null - (指向)最后一个结点/尾结点:
transient Node<E> last;
// 初始为null
Node类型:
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
item存放的是当前元素;next指向下一个结点;prev指向上一个结点。
Node的有参构造方法的三个参数分别是前一个结点、存储的元素、后一个结点,调用这个构造方法构造出当前对象。
添加元素的add方法(尾插法):
public boolean add(E e) {
linkLast(e);
return true;
}
进入到linkLast方法:
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++;
}
ArrayList和LinkedList区别
- ArrayList:必须开辟连续空间,查询快,增删慢。
- LinkedList:无需开辟连续空间,查询慢,增删快。
泛型概述
-
泛型其本质是参数化类型,把类型作为参数传递。
-
常见形式有泛型类、泛型接口、泛型方法。
-
语法:
- 类名 < T >,T是类型占位符,表示一种引用类型,编写多个时用逗号隔开
- 泛型只能使用引用类型
- 不同泛型类型的对象之间不能相互赋值
- 声明泛型变量,不能使用new来创建,因为泛型是不确定的类型,也可能拥有私密的构造方法。
- 不能创建泛型静态常量,因为不明确类型,无法初始化
-
好处:
-
提高代码的重用性。(比如代替方法重载)
-
防止类型转换异常,提高代码的安全性:
-
ArrayList arrayList=new ArrayList(); arrayList.add("xxx"); arrayList.add("yyy"); arrayList.add(22); arrayList.add(33); for (Object object:arrayList) { String s= (String)object; // 异常 ---> Integer 不能转为 String System.out.println(s); }
-
泛型类
/**
* 泛型类
*/
class myGeneric<T>{
//1.创建泛型变量
T t;
//2.泛型作为方法的参数
public void show(T t) {
System.out.println(t);
}
//泛型作为方法的返回值
public T getT() {
return t;
}
}
public class testFX {
public static void main(String[] args) {
//使用泛型类创建对象
myGeneric<String> myGeneric1=new myGeneric<String>();
myGeneric1.t="tang";
myGeneric1.show("he");
System.out.println(myGeneric1.getT());
myGeneric<Integer> myGeneric2=new myGeneric<Integer>();
myGeneric2.t=10;
myGeneric2.show(20);
System.out.println(myGeneric2.getT());
}
}
-----------执行结果-------
he
tang
20
10
泛型接口
/**
* 泛型接口
* 语法:接口名<T>
*/
public interface MyInterface<T> {
//创建常量
String nameString="tang";
T server(T t);
}
/**
* 实现接口时确定泛型类
*/
public class MyInterfaceImpl implements MyInterface<String>{
@Override
public String server(String t) {
System.out.println(t);
return t;
}
}
//测试类
MyInterfaceImpl myInterfaceImpl=new MyInterfaceImpl();
System.out.println(myInterfaceImpl.server("xxx"));
----------执行结果-------
xxx 泛型类中输出的
xxx 测试类中输出的,因为返回了 t
/**
* 实现接口时不确定泛型类
*/
public class MyInterfaceImpl2<T> implements MyInterface<T>{
@Override
public T server(T t) {
System.out.println(t);
return t;
}
}
//测试类
MyInterfaceImpl2<Integer> myInterfaceImpl2=new MyInterfaceImpl2<Integer>();
myInterfaceImpl2.server(2000);
----执行结果----
2000
泛型方法
/**
* 泛型方法
* 语法:<T> 返回类型
*/
public class MyGenericMethod {
public <T> void show(T t) { // 可以是静态的,也可以不是
System.out.println("泛型方法"+t);
}
}
//测试类
MyGenericMethod myGenericMethod=new MyGenericMethod();
myGenericMethod.show("tang"); //泛型方法tang
myGenericMethod.show(200); //泛型方法200
myGenericMethod.show(3.14); //泛型方法3.14
泛型集合
-
概念:参数化类型、类型安全的集合,强制集合元素的类型必须一致。
-
特点:
- 编译时即可检查,而非运行时抛出异常。
- 访问时,不必类型转换。
- 不同泛型之间引用不能相互赋值,泛型不存在多态。
之前我们在创建LinkedList类型对象的时候并没有使用泛型,但是进到它的源码中会发现:
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable{略}
它是一个泛型类,而之前使用的时候并没有传递,说明java语法是允许的,这个时候传递的类型是Object类,虽然它是所有类的父类,可以存储任意的类型,但是在遍历、获取元素时需要原来的类型就要进行强制转换。这个时候就会出现一些问题,假如往链表里存储了许多不同类型的数据,在强转的时候就要判断每一个原来的类型,这样就很容易出现错误。
public static void main(String[] args) {
ArrayList<String> arrayList=new ArrayList<String>();
arrayList.add("xxx");
arrayList.add("yyy");
//arrayList.add(22); integer类型不能加入
for (String string:arrayList) { // 少一步强制类型转换 Object-> String
System.out.println(string);
}
ArrayList<Student> arrayList1=new ArrayList<Student>();
Student s1=new Student("张三",18);
Student s2=new Student("李四", 20);
Student s3=new Student("王五", 19);
arrayList1.add(s1);
arrayList1.add(s2);
arrayList1.add(s3);
Iterator<Student> it=arrayList1.iterator();
while (it.hasNext()){
Student s=it.next(); // 少一步强制类型转换 Object-> Student
System.out.println(s.toString());
}
}
Set集合概述
Set子接口
- 特点:无序、无下标、元素不可重复。
- 方法:全部继承自Collection中的方法。
/**
* 测试Set接口的使用
* 1.添加数据
* 2.删除数据
* 3.遍历【重点】
* 4.判断
*/
public static void main(String[] args) {
Set<String> set=new HashSet<String>();
//1.添加数据
set.add("tang");
set.add("he");
set.add("yu");
System.out.println("数据个数:"+set.size());
System.out.println(set.toString());// 无序输出
//2.删除数据
/*
* set.remove("tang"); System.out.println(set.toString());
*/
//3.遍历【重点】
//3.1 使用增强for
for (String string : set) {
System.out.println(string);
}
//3.2 使用迭代器
Iterator<String> iterator=set.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
//4.判断
System.out.println(set.contains("tang"));
System.out.println(set.isEmpty());
}
Set实现类
HashSet【重点】
- 基于HashCode实现元素不重复。
- 当存入元素的哈希码相同时,会调用equals进行确认,如结果为true,则拒绝后者存入。
- 存储结构:哈希表(数组+链表+(红黑树))
/**
* 人类
*/
public class Person {
private String name;
private int age;
public Person(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 "Peerson [name=" + name + ", age=" + age + "]";
}
}
/**
* HashSet集合的使用
* 1.添加元素
* 2.删除元素
* 3.遍历
* 4.判断
*/
public static void main(String[] args) {
HashSet<Person> hashSet=new HashSet<Person>();
Person p1=new Person("tang",21);
Person p2=new Person("he", 22);
Person p3=new Person("yu", 21);
//1.添加元素
hashSet.add(p1);
hashSet.add(p2);
hashSet.add(p3);
hashSet.add(p3); //重复,添加失败
//直接new一个相同属性的对象,依然会被添加进去,因为不是同一个对象。
// 重写之后就不能添加进去
hashSet.add(new Person("yu", 21));
System.out.println(hashSet.toString());
//2.删除元素
hashSet.remove(p2);
hashSet.remove(new Person("yu", 21)); // 重写之后就能删除
//3.遍历
//3.1 增强for
for (Person person : hashSet) {
System.out.println(person);
}
//3.2 迭代器
Iterator<Person> iterator=hashSet.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
//4.判断
System.out.println(hashSet.isEmpty());
System.out.println(hashSet.contains(new Person("tang", 21)));
//直接new一个相同属性的对象结果输出是false,因为不是同一个对象。
//重写之后就能输出true
}
hashSet存储过程:
-
根据哈希码(Object中的native hashcode()方法)计算保存的位置,如果位置为空,则直接保存,否则执行第二步。
-
执行equals方法(继承自Object,比较的是地址),如果方法返回true,认为是重复则拒绝存储;如果方法返回false,顺利存储形成链表。
- 对象调用hashCode()方法获得自身哈希码,不同对象可能有相同哈希码也可能不同哈希码;相同对象一定有相同哈希码;相同哈希码不一定相同对象;不同哈希码一定不相同对象
- 哈希码相同,位置一定相同;哈希码不同,位置有可能相同有可能不同;
- 要实现“相同属性便认为是同一个对象”,同时重写hashCode和equals方法:
// 快捷键:右键Generate(alt+insert)--> equals() and hashCode()
// 在Person类中重写这两个方法
@Override
public int hashCode() {
int n1 = this.age;
int n2 = this.name.hashCode();
return n1+n2;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (obj instanceof Person){
Person p = (Person) obj;
if((p.name==this.name)&&(p.age==this.age)){
return true;
}
}
return false;
}
快捷键的hashCode方法里为什么要使用31这个数字大概有两个原因:
- 31是一个质数,这样的数字在计算时可以尽量减少散列冲突。
- 可以提高执行效率,因为31*i=(i<<5)-i,31乘以一个数可以转换成移位操作,这样能快一点
TreeSet
- 基于排序顺序实现元素不重复。
- 实现了SortedSet接口,对集合元素自动排序。
- 元素对象的类型必须实现Comparable接口,指定排序规则。
- 通过CompareTo方法确定是否为重复元素,compareTo方法返回0,认为是重复元素
- 存储结构:红黑树(一种二叉查找树、弱平衡二叉树)
/**
* 使用TreeSet保存数据
*/
public static void main(String[] args) {
TreeSet<Person> persons=new TreeSet<Person>();
Person p1=new Person("tang",21);
Person p2=new Person("he", 22);
Person p3=new Person("yu", 21);
//1.添加元素
persons.add(p1);
persons.add(p2);
persons.add(p3);
//注:直接添加会报类型转换错误,需要Student类实现并重写
// Comparable泛型接口中的compareTo()方法,可选择是否指定泛型类型
System.out.println(persons.toString());
System.out.println(persons.size());
//2.删除元素
//persons.remove(p1); // 能删除
persons.remove(new Person("he", 22));//能删除 因为compareTo比较姓名和年龄
System.out.println(persons.toString());
System.out.println(persons.size());
//3.遍历(略)
//4.判断
System.out.println(persons.contains(new Person("yu", 21)));// true 因为compareTo()
}
查看Comparable接口的源码,发现只有一个compareTo抽象方法,在Person类中实现它:
public class Person implements Comparable<Person>{
@Override
//1.先按姓名比
//2.再按年龄比
public int compareTo(Person o) {
int n1=this.getName().compareTo(o.getName());
int n2=this.age-o.getAge();
return n1==0?n2:n1;
}
}
除了实现Comparable接口里的比较方法,TreeSet也提供了一个带比较器Comparator的构造方法,使用匿名内部类来实现它:
/**
* TreeSet的使用
* Comparator:实现定制比较(比较器)
*/
public static void main(String[] args) {
// 创建集合时就定制了比较规则
TreeSet<Person> persons=new TreeSet<Person>(new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
// 先按年龄比较
// 再按姓名比较
int n1=o1.getAge()-o2.getAge();
int n2=o1.getName().compareTo(o2.getName());
return n1==0?n2:n1;
}
});
Person p1=new Person("tang",21);
Person p2=new Person("he", 22);
Person p3=new Person("yu", 21);
persons.add(p1);
persons.add(p2);
persons.add(p3);
System.out.println(persons.toString());
}
接下来我们来做一个小案例:
/**
* 要求:使用TreeSet集合实现字符串按照长度进行排序
* helloworld tangrui hechengyang wangzixu yuguoming
* Comparator接口实现定制比较
*/
public static void main(String[] args) {
TreeSet<String> treeSet=new TreeSet<String>(new Comparator<String>() {
@Override
//先比较字符串长度
//再比较字符串
public int compare(String o1, String o2) {
int n1=o1.length()-o2.length();
int n2=o1.compareTo(o2);
return n1==0?n2:n1;
}
});
treeSet.add("helloworld");
treeSet.add("tangrui");
treeSet.add("hechenyang");
treeSet.add("yuguoming");
treeSet.add("wangzixu");
System.out.println(treeSet.toString());
//输出[tangrui, wangzixu, yuguoming, hechenyang, helloworld]
}
Map集合
-
Map接口的特点:
- 用于存储任意键值对(Key-Value)。
- 键:无序、无下标、不允许重复(唯一)。
- 值:无序、无下标、允许重复。
Map父接口
-
特点:存储一对数据(Key-Value),无序、无下标,键不可重复,值可重复。
-
方法:
V put(K key,V value);
//将对象存入到集合中,关联键值。key重复则覆盖原值。Object get(Object key);
//根据键获取相应的值。Set<K> keySet();
//返回此映射中所有key的 Set 集合Collection<V> values();
//返回包含所有值的Collection集合。Set<Map.Entry<K,V>> entrySet();
// 返回此映射中包含的映射关系的 Set 集合
/** * Map接口的使用 */ public static void main(String[] args) { Map<String,Integer> map=new HashMap<String, Integer>(); //1.添加元素 map.put("tang", 21); map.put("he", 22); map.put("fan", 23); System.out.println(map.toString()); //2.删除元素 map.remove("he"); // remove(Object key); System.out.println(map.toString()); //3.遍历 //3.1 使用keySet(); for (String key : map.keySet()) { System.out.println(key+" "+map.get(key)); } //3.2 使用entrySet(); 效率较高 把键值对包装成一个 Entry for (Map.Entry<String, Integer> entry : map.entrySet()) { System.out.println(entry.getKey()+" "+entry.getValue()); } }
Map集合的实现类
HashMap【重点】
-
线程不安全(单线程使用),运行效率快;允许用null作为key或是value。
-
存储结构:哈希表(数组+链表+(红黑树))
/** * 学生类 */ public class Student { private String name; private int id; public Student(String name, int id) { super(); this.name = name; this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getId() { return id; } public void setId(int id) { this.id = id; } @Override public String toString() { return "Student [name=" + name + ", age=" + id + "]"; } }
/** * HashMap的使用 */ public static void main(String[] args) { HashMap<Student, String> hashMap=new HashMap<Student, String>(); Student s1=new Student("tang", 36); Student s2=new Student("yu", 101); Student s3=new Student("he", 10); //1.添加元素 hashMap.put(s1, "成都"); hashMap.put(s2, "杭州"); hashMap.put(s3, "郑州"); // hashMap.put(s3,"上海"); //添加失败,但会更新值 hashMap.put(new Student("he", 10),"上海");//添加成功,不过两个属性一模一样 System.out.println(hashMap.toString()); //2.删除元素 hashMap.remove(s3); System.out.println(hashMap.toString()); //3.遍历 //3.1 使用keySet()遍历 for (Student key : hashMap.keySet()) { System.out.println(key+" "+hashMap.get(key)); } //3.2 使用entrySet()遍历 for (Map.Entry<Student, String> entry : hashMap.entrySet()) { System.out.println(entry.getKey()+" "+entry.getValue()); } //4.判断 System.out.println(hashMap.containsKey(new Student("he", 10)));//重写之前false 重写之后true System.out.println(hashMap.containsValue("成都")); //true }
注:和之前说过的HashSet类似,重复依据是hashCode和equals方法,重写即可:
// 在Student类中重写这两个方法 @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + id; result = prime * result + ((name == null) ? 0 : name.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Student other = (Student) obj; if (id != other.id) return false; if (name == null) { if (other.name != null) return false; } else if (!name.equals(other.name)) return false; return true; }
HashMap源码分析
-
默认初始化容量:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
-
最大容量:
static final int MAXIMUM_CAPACITY = 1 << 30;
-
默认加载因子:
static final float DEFAULT_LOAD_FACTOR = 0.75f;
-
链表调整为红黑树的链表长度最小阈值(JDK1.8):
static final int TREEIFY_THRESHOLD = 8;
-
红黑树调整为链表的链表长度最大阈值(JDK1.8):
static final int UNTREEIFY_THRESHOLD = 6;
-
链表调整为红黑树的哈希数组最小阈值(JDK1.8):
static final int MIN_TREEIFY_CAPACITY = 64;
-
HashMap存储的数组:
transient Node<K,V>[] table;
-
HashMap存储的元素个数:
transient int size;
- 默认加载因子是什么?
- 就是判断数组是否扩容的一个因子。假如数组容量为100,如果HashMap的存储元素个数超过了100*0.75=75,那么就会进行扩容。
- 链表调整为红黑树的链表长度阈值是什么?
- 假设在数组中下标为3的位置已经存储了数据,当新增数据时通过哈希码得到的存储位置又是3,那么就会在该位置形成一个链表,当链表过长时就会转换成红黑树以提高执行效率,这个阈值就是链表转换成红黑树的最小链表长度;
- 红黑树调整为链表的链表长度阈值是什么?
- 当红黑树的元素个数小于该阈值时就会转换成链表。
- 链表调整为红黑树的数组最小阈值是什么?
- 并不是只要链表长度大于8就可以转换成红黑树,在前者条件成立的情况下,数组的容量必须大于等于64才会进行转换。
HashMap的数组table存储的就是一个个的Node<K,V>类型,很清晰地看到有一对键值,还有一个指向next的指针(以下只截取了部分源码):
static class Node<K,V> implements Map.Entry<K,V> { final K key; V value; Node<K,V> next; }
之前的代码中在new对象时调用的是HashMap的无参构造方法,进入到该构造方法的源码查看一下:
public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted }
发现没什么内容,只是赋值了一个默认加载因子;而在上文我们观察到源码中table和size都没有赋予初始值,说明刚创建的HashMap对象没有分配容量,并不拥有默认的16个空间大小,这样做的目的是为了节约空间,此时table为null,size为0。
当我们往对象里添加元素时调用put方法:
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
put方法把key和value传给了putVal,同时还传入了一个hash(Key)所返回的值,这是一个产生哈希值的方法,再进入到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; if ((tab = table) == null || (n = tab.length) == 0) // 把table赋值给tab n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) // 找位置,判断是否为空,为空直接加入 tab[i] = newNode(hash, key, value, null); else{ //略 } }
这里面创建了一个tab数组和一个Node变量p,第一个if实际是判断table是否为空,而我们现在只关注刚创建HashMap对象时的状态,此时tab和table都为空,满足条件,执行内部代码,这条代码其实就是把resize()所返回的结果赋给tab,n就是tab的长度,resize顾名思义就是重新调整大小。查看resize()源码(部分):
final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; if (oldCap > 0); else if (oldThr > 0); else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; return newTab; }
该方法首先把table及其长度赋值给oldTab和oldCap;threshold是扩容阈值的意思(16*0.75),此时为0,所以前两个if先不管,最后else里newCap的值为默认初始化容量16;往下创建了一个newCap大小的数组并将其赋给了table,刚创建的HashMap对象就在这里获得了初始容量。然后我们再回到putVal方法,第二个if就是根据哈希码得到的tab中的某个位置是否为空,为空便直接添加元素,此时数组中无元素所以直接添加。至此HashMap对象就完成了第一个元素的添加。当添加的元素超过(16乘0.75=)12时,就会进行扩容:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict){ if (++size > threshold) resize(); }
扩容的代码如下(部分):
final Node<K,V>[] resize() { int oldCap = (oldTab == null) ? 0 : oldTab.length; int newCap; if (oldCap > 0) { if (oldCap >= MAXIMUM_CAPACITY) {//略} else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) } }
核心部分是else if里的左移操作,也就是说每次扩容都是原来大小的两倍。
- 默认加载因子是什么?
-
额外说明的一点是在JDK1.8以前链表是头插入,JDK1.8以后链表是尾插入。
-
HashMap刚创建时,table为null,为了节省空间,当添加第一个元素时,table容量调整为16
HashSet源码分析
了解完HashMap之后,再回过头来看之前的HashSet源码,为什么放在后面写你们看一下源码就知道了(部分):
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
{
private transient HashMap<E,Object> map;
private static final Object PRESENT = new Object();
public HashSet() {
map = new HashMap<>();
}
}
可以看见HashSet的存储结构就是HashMap,那它的存储方式是怎样的呢?可以看一下add方法:
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
发现它的add方法调用的就是map的put方法,把元素作为map的key传进去的。。
Hashtable
-
线程安全,运行效率慢;不允许null作为key或是value。
(这个集合在开发过程中已经不用了,稍微了解即可)
Properties
- Hashtable的子类,要求key和value都是String。通常用于配置文件的读取。
它继承了Hashtable的方法,与流关系密切,此处不详解。
TreeMap
- 实现了SortedMap接口(是Map的子接口),可以对key自动排序。
- 存储结构:红黑树
/**
* TreeMap的使用
*/
public static void main(String[] args) {
TreeMap<Student, Integer> treeMap=new TreeMap<Student, Integer>();
Student s1=new Student("tang", 36);
Student s2=new Student("yu", 101);
Student s3=new Student("he", 10);
//1.添加元素
treeMap.put(s1, 21);
treeMap.put(s2, 22);
treeMap.put(s3, 21);
treeMap.put(new Student("he", 10), 91);// 添加失败,因为compareTo根据id进行比较,会认为这是同一个对象,但是value会覆盖 即91覆盖21
//不能直接打印,需要实现Comparable接口,因为红黑树需要比较大小,比较key
System.out.println(treeMap.toString());
//2.删除元素
treeMap.remove(new Student("he", 10));//
System.out.println(treeMap.toString());
//3.遍历
//3.1 使用keySet()
for (Student key : treeMap.keySet()) {
System.out.println(key+" "+treeMap.get(key));
}
//3.2 使用entrySet()
for (Entry<Student, Integer> entry : treeMap.entrySet()) {
System.out.println(entry.getKey()+" "+entry.getValue());
}
//4.判断
System.out.println(treeMap.containsKey(s1));
System.out.println(treeMap.isEmpty());
}
在学生类中实现Comparable接口:
public class Student implements Comparable<Student>{
@Override
public int compareTo(Student o) {
int n1=this.id-o.id;
return n1;
}
除此之外还可以使用比较器来定制比较:
TreeMap<Student, Integer> treeMap2=new TreeMap<Student, Integer>(new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
// 略
return 0;
}
});
TreeSet源码
和HashSet类似,放在TreeMap之后讲便一目了然(部分):
public class TreeSet<E> extends AbstractSet<E>
implements NavigableSet<E>, Cloneable, java.io.Serializable
{
private transient NavigableMap<E,Object> m;
private static final Object PRESENT = new Object();
TreeSet(NavigableMap<E,Object> m) {
this.m = m;
}
public TreeSet() {
this(new TreeMap<E,Object>()); // 调用TreeSet的构造方法
}
}
TreeSet的存储结构实际上就是TreeMap,再来看其存储方式:
public boolean add(E e) {
return m.put(e, PRESENT)==null;
}
它的add方法调用的就是TreeMap的put方法,将元素作为key传入到存储结构中。
Collections工具类
-
概念:集合工具类,定义了除了存取以外的集合常用方法。
-
方法:
public static void reverse(List<?> list)
//反转集合中元素的顺序public static void shuffle(List<?> list)
//随机重置集合元素的顺序public static void sort(List<T> list)
//升序排序(元素类型必须实现Comparable接口)
/** * 演示Collections工具类的使用 * */ public static void main(String[] args) { List<Integer> list=new ArrayList<Integer>(); list.add(20); list.add(10); list.add(30); list.add(90); list.add(70); //sort排序 System.out.println(list.toString()); Collections.sort(list); System.out.println(list.toString()); System.out.println("---------"); //binarySearch二分查找 返回目标下标 int i=Collections.binarySearch(list, 10); System.out.println(i); //复制,该方法要求目标元素容量大于等于源目标,多余的位置保留类型默认值 List<Integer> list2=new ArrayList<Integer>(); for(int i1=0;i1<5;i1++) { list2.add(0); } Collections.copy(list2, list); System.out.println(list2.toString()); //reserve反转 Collections.reverse(list2); System.out.println(list2.toString()); //shuffle 打乱 Collections.shuffle(list2); System.out.println(list2.toString()); //补充:list转成数组 Integer[] arr=list.toArray(new Integer[0]);// 主要是用这个Integer类型 System.out.println(arr.length); //补充:数组转成集合 String[] nameStrings= {"tang","he","yu"}; //受限集合,不能添加和删除 List<String> list3=Arrays.asList(nameStrings); System.out.println(list3); // int[] nums={1,2,4,5}; 基本类型数组转成集合时需要修改为包装类 Integer[] num={1,2,4,5}; List<Integer> list5=Arrays.asList(num); System.out.println(list5); }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)