目录
数组特点:
长度固定:
创建时需要指定长度,并且长度在创建后不可改变
相同数据类型:
例如int数组只能存储int类型的数据
连续内存分配:
堆空间为数组开辟的内存是连续的
导致在插入和删除元素时需要元素整体移动,效率低下
随机访问:
由于数组中的元素在内存中是连续存储的
可以通过索引来访问,时间复杂度为O(1)
显而易见
如果我们要存储多个数据类型不一致
或者个数不固定时
数组就无法满足我们的需求了
这时就需要使用 java 提供的集合框架了
一、集合概述
1、集合
①基础概念
java中,集合是一种用于存储和操作一组对象的数据结构
它提供了一组接口和类,用于处理和操作对象的集合
②与数组的对比
为了更好地理解集合,我们把集合和数组放在一起来看:
首先他们都可以存储多个元素值
区别在于:
1. 数组的长度是固定的
集合的长度是可变的
2. 数组中存储的是同一类型的元素
集合中存储的数组可以是不同类型的
3. 数组可以存放基本类型数据或引用类型变量
集合中只能存放引用类型变量
4. 数组除了拥有 length属性 和 从Object中继承过来的方法之外,数组对象调用不了其他的属性和方法了
集合框架由 java.util 包下多个接口和实现类组成,定义并实现了很多方法,功能强大
只能存引用类型变量?
那下面这些情况是怎么回事?
collection.add(1);
int a = 1;
collection.add(a);
Integer integer = 1;
collection.add(integer);
实际情况是
前两种自动装箱
最后一种直接存储 Integer 对象
所以“集合只能存储引用类型”这个说法是正确的
③集合分类:
1.单列集合(Single Column Collection)
根接口: java.util.Collection
单列集合是指每个集合元素只包含一个单独的对象,它是集合框架中最简单的形式
Collection接口结构图(重要部分,并非全部):
1.多列集合(Multiple Column Collection)
根接口: java.util.Map
多列集合是指每个集合元素由多个列(字段)组成,可以同时存储和操作多个相关的值
Map接口结构图如下(重要部分,并非全部):
2、集合框架
①基础概念
集合框架
java中用于表示和操作集合的一组类和接口
位于 java.util 包中
包括:
Collection 集合接口
List 列表接口
Map 映射接口
Set 类
... ...
目的:
提供一种通用的方式来存储和操作对象的集合
无论集合的具体实现方式如何
用户都可以使用统一的接口和方法来操作集合
怎么理解 集合 和 集合框架 呢 ?
如果把一个个集合看作一个个收纳盒
那么集合框架就是 收纳指南
上面记录了:
不同类型的收纳盒、
统一的操作标准、
还有现成的使用方法
②三要素
1.接口
整个集合框架的上层结构,都是用接口进行组织的
接口中定义了集合中必须要有的基本方法
通过接口还把集合划分成了几种不同的类型,每一种集合都有自己对应的接口
2、实现类
对于上层使用接口划分好的集合种类,每种集合接口都会有对应的实现类
每一种接口的实现类很可能有多个,每个的实现方式也各有不同
3、数据结构
每个实现类都实现了接口中所定义的最基本的方法
例如对数据的存储、检索、操作等方法
但是不同的实现类,它们存储数据的方式不同,也就是使用的数据结构不同
③集合框架继承体系图(重要部分,并非全部)
二、单列集合
1、概述
单列集合是指每个集合元素只包含一个单独的对象
单列集合的根接口是 java.util.Collection
根接口下面又有两个子接口:List 和 Set
如何理解这句话呢?
我们把单列和多列放一起比较
就好比有一个班级花名册
单列集合每一行只有单独的名字信息,而多列集合一行有多个信息:
单列 双列
![]()
再比如说有一列火车
单列火车是这样的:
[苹果车厢] [牛奶车厢] [面包车厢] [鸡蛋车厢] // 每节车厢只装一种商品
双列火车是这样的:
[苹果-5元车厢] [牛奶-10元车厢] [面包-8元车厢] // 每节车厢装两种信息:商品名和价格
2、Collection接口
单列集合的父接口
其中定义了单列集合通用的一些方法
Collection接口的实现类都可以使用这些方法
①Collection基础方法
package java.util;
public interface Collection<E> extends Iterable<E> {
//省略...
//向集合中添加元素
boolean add(E e)
//清空集合中所有的元素。
void clear()
//判断当前集合中是否包含给定的对象。
boolean contains(Object o)
//判断当前集合是否为空。
boolean isEmpty()
//把给定的对象,在当前集合中删除。
boolean remove(Object o)
//返回集合中元素的个数。
int size()
//把集合中的元素,存储到数组中。
Object[] toArray()
}
示例:
import java.util.ArrayList;
import java.util.Collection;
public class Test {
// 集合中 一般 也放 同一种类型数据
public static void main(String[] args) {
// 1.接口 引用 = new 实现类(实参);
Collection coll = new ArrayList();
if (coll.isEmpty())
System.out.println("coll is Empty!");
// 2.任何引用类型都可以放入
// 自动扩容
coll.add("hello"); //String
Integer i = 12;
coll.add(i);
coll.add(2.3); // Double
coll.add(1.2F); // Float
coll.add('a'); // Character
int[] arr = { 1, 2, 3 };
coll.add(arr); //int[]
// coll.add(new Student());
// 3.输出 coll.toString()
System.out.println(coll);
System.out.println("size: " + coll.size());
System.out.println("--------------");
// 4.清空 coll
coll.clear();
System.out.println(coll);
// 5.判断是否为空
if (coll.isEmpty())
System.out.println("coll is Empty!");
else
System.out.println("coll is not empty!");
}
}
此时动手实操的同学会发现代码爆黄线了
为什么呢?
其实是我们的格式有点小问题
区别于之前的书写格式,集合是这样的:
接口类型<存储的数据类型> 接口引用名 = new 实现类<>(构造方法实参);
例: Collection<String> coll = new ArrayList<>();
还可以写成:
Collection<String> coll = new ArrayList<String>();
Collection coll = new ArrayList();
需要注意的是,前两种明确了数据类型,就只能存储这一种
第三种可以存储多种,但是一般情况下存储一种数据类型
②Collection泛型参数方法补充
package java.util;
public interface Collection<E> extends Iterable<E> {
//省略...
//把一个指定集合中的所有数据,添加到当前集合中
boolean addAll(Collection<? extends E> c)
//判断当前集合中是否包含给定的集合的所有元素。
boolean containsAll(Collection<?> c)
//把给定的集合中的所有元素,在当前集合中删除。
boolean removeAll(Collection<?> c)
//判断俩个集合中是否有相同的元素,如果有当前集合只保留相同元素,如果没有当前集合元素清空
boolean retainAll(Collection<?> c)
//把集合中的元素,存储到数组中,并指定数组的类型
<T> T[] toArray(T[] a)
//返回遍历这个集合的迭代器对象
Iterator<E> iterator()
}
示例:
import java.util.ArrayList;
import java.util.Collection;
//泛型方法测试
public class Test03_Element {
public static void main(String[] args) {
//1.实例化两个集合对象,专门存放String类型元素
// 集合实例化对象 固定写法
Collection<String> c1 = new ArrayList<>();
Collection<String> c2 = new ArrayList<>();
//2.分别往c1和c2集合中添加元素
String s1 = "hello";
String s2 = "world";
c1.add(s1);
c1.add(s2);
String s3 = "nihao";
String s4 = "hello";
String s5 = "okok";
c2.add(s3);
c2.add(s4);
c2.add(s5);
System.out.println("c1: " + c1);
System.out.println("c2: " + c2);
System.out.println("-------------");
//3.将c2集合整体添加到c1中
c1.addAll(c2);
System.out.println("c1.size: " + c1.size());
System.out.println("after addAll(c2), c1: " + c1);
System.out.println("-------------");
//4.判断是否包含指定元素
boolean f = c1.contains("hello");
System.out.println("contains hello: " + f);
//5.创建s6对象,判断集合中是否包含该对象
// 注意: s6的地址 和 "world"地址不一样
// s6是堆中临时new出来的,"world"存在堆中的字符串常量池中
String s6 = new String("world");
// 结果显示true,说明集合contains方法借助equals方法进行比较,而非 ==
f = c1.contains(s6);
System.out.println("contains(s6): " + f);
System.out.println("-------------");
//6.判断是否包含c2对象
f = c1.containsAll(c2);
System.out.println("containsAll(c2): " + f);
System.out.println("-------------");
//7.删除指定元素【底层借助equals比较,然后删除】
f = c1.remove(s6);
System.out.println("remove(s6): " + f);
System.out.println("after remove, c1: " + c1);
System.out.println("-------------");
//8.删除c2整个集合【底层实现:遍历c2,逐个元素equals比较,然后删除】
f = c1.removeAll(c2);
System.out.println("removeAll(c2): " + f);
System.out.println("after remove, c1: " + c1);
}
}
3、List接口
继承了 Collection 接口
①特点:
——有序集合
——带索引的集合,可以通过索引精确查找对应元素
——可以存放重复元素(包括null)
——默认尾部添加元素
②常用方法:
除了从 Collection 中继承过来的方法,除此之外还有:
//返回集合中指定位置的元素。
E get(int index);
//用指定元素替换集合中指定位置的元素,并返回被替代的旧元素。
E set(int index, E element);
//将指定的元素,添加到该集合中的指定位置上。
void add(int index, E element);
//从指定位置开始,把另一个集合的所有元素添加进来
boolean addAll(int index, Collection<? extends E> c);
//移除列表中指定位置的元素, 并返回被移除的元素。
E remove(int index);
//查收指定元素在集合中的所有,从前往后查到的第一个元素(List集合可以重
复存放数据)
int indexOf(Object o);
//查收指定元素在集合中的所有,从后往前查到的第一个元素(List集合可以重
复存放数据)
int lastIndexOf(Object o);
//根据指定开始和结束位置,截取出集合中的一部分数据
List<E> subList(int fromIndex, int toIndex);
示例:
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class Test {
public static void main(String[] args) {
//1.创建List集合对象
List<String> list = new ArrayList<>();
//2.添加元素,默认尾部添加
list.add("hello1");
list.add("hello2");
list.add("hello3");
list.add("hello1");
System.out.println(list);
//3.指定位置添加元素
// add(int index,String s)
list.add(1, "world");
System.out.println(list);
//4.删除索引位置为2的元素
boolean f = list.remove(2);
System.out.println("remove(2): " + f);
System.out.println("after remove: " + list);
//5.修改指定位置元素
list.set(0, "briup");
System.out.println(list);
//6.借助get方法遍历集合
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
System.out.println("-----------------");
//7.使用foreach遍历
for(Object obj : list){
System.out.println(obj);
}
System.out.println("-----------------");
//8.使用迭代器遍历
Iterator<String> it = list.iterator();
while(it.hasNext()){
String str = it.next();
System.out.println(str);
}
}
}
③实现类:
1> ArrayList
是 List 的一个实现类
是最常用的一种 List 类型集合
extends AbstractList<E> extends AbstractCollection<E>
特点:
1. ArrayList集合底层是数组,查询快,增删慢
适用业务场景:查询功能多,增删功能少
eg:学生管理系统
2. ArrayList集合线程不安全
代码示例与 LinkedList 放在一起对比
2> LinkedList
实现了List接口
extends AbstractSequentialList<E> extends AbstractList<E>
特点:
1. LinkedList 集合底层是双向链表,查询慢,增删快
适用业务场景:增删功能多,查询功能少
eg:浏览器的前进后退
2. LinkedList 集合线程也不安全
特点验证:
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
public class Test {
public static void main(String[] args) {
//操作集合的次数
final int NUM = 100000;
//1.实例化集合对象
List<String> list = new ArrayList<>();
//List<String> list = new LinkedList<>();
//2.开启计时,往集合种放入 100000 个元素
long start1 = System.currentTimeMillis();
for (int i = 0; i < NUM; i++) {
list.add(0,"hello"+i);
}
long end1 = System.currentTimeMillis();
//3.输出时长
System.out.println(list.getClass().getSimpleName()+"插入"+NUM+"条数据耗时"+(end1-start1)+"毫秒");
//4.开启计时,从集合种取 100000 个元素
long start2 = System.currentTimeMillis();
for(int i = 0; i < list.size(); i++){
list.get(i);
}
long end2 = System.currentTimeMillis();
//5.输出时长
System.out.println(list.getClass().getSimpleName()+"检索"+NUM+"条数据耗时"+(end2-start2)+"毫秒");
}
}
//运行效果:
//根据电脑的当前情况,每次运行的结果可能会有差异
//以下是我的电脑运行的实验结果
ArrayList插入100000条数据耗时508毫秒
ArrayList检索100000条数据耗时2毫秒
LinkedList插入100000条数据耗时22毫秒
LinkedList检索100000条数据耗时17709毫秒
System.currentTimeMillis();
//获取当前时刻的时间戳
3> Vector
实现了 List 接口
extends AbstractList<E> extends AbstractCollection<E>
特点:
1. Vector 集合底层是数组,查询快,增删慢
2. Vector 集合多线程安全,效率低
4、Set接口
继承了 Collection 接口
①特点:
——无序集合,不按 add 顺序添加元素
——不带下标(索引)
——元素不能重复
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
public class Test {
public static void main(String[] args){
//1.实例化Set集合,指向HashSet实现类对象
Set<String> set = new HashSet<>();
set.add("hello1");
set.add("hello2");
set.add("hello3");
set.add("hello4");
set.add("hello5"); //添加失败 重复元素
set.add("hello5"); //添加失败 重复元素
//加强for循环遍历
for(String obj : set){
System.out.println(obj);
}
System.out.println("-----------------");
//迭代器遍历
Iterator<String> it = set.iterator();
while(it.hasNext()){
Object obj = it.next();
System.out.println(obj);
}
}
}
//输出结果:
hello1
hello4
hello5
hello2
hello3
----------
省略...
②常用方法:
基本都是Collection继承过来的,这里就不再赘述
③实现类:
1> HashSet
底层是哈希表
extends AbstractSet<E> implements Set<E>
特点:
1. 无序性:存储顺序和插入顺序无关
2. 唯一性:元素不重复,null也只能存一个
3. 高效性:哈希表的特点
需要注意的是,由于HashSet没有重写 equals方法 和 hashCode方法
所以存储 数据相同的自定义类的对象 时会认为它们是不同的
比如:
import java.util.*;
class Student {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
// 注意:这里没有重写 equals 和 hashCode 方法
@Override
public String toString() {
return "Student{name='" + name + "', age=" + age + "}";
}
}
public class HashSetDuplicateExample {
public static void main(String[] args) {
Set<Student> set = new HashSet<>();
// 添加四个"相同"的Student对象
set.add(new Student("Alice", 20));
set.add(new Student("Alice", 20));
set.add(new Student("Alice", 20));
set.add(new Student("Alice", 20));
System.out.println("HashSet大小: " + set.size()); // 输出 4
System.out.println("HashSet内容: " + set);
}
}
所以要往 HashSet 集合存储自定义类
一定要记得重写自定义类中的equals方法和hashCode方法
2> TreeSet
底层是二叉树
TreeSet是SortedSet(Set接口的子接口)的实现类
特点:
1. 有序性:插入的元素会自动排序
2. 唯一性:元素不重复,null也只有一个
3. 高效性:二叉树的特点
入门案例一:
import java.util.Set;
import java.util.TreeSet;
public class Test{
public static void main(String[] args){
//1.实例化集合对象
Set<Integer> set = new TreeSet<>();
//2.添加元素
set.add(3);
set.add(5);
set.add(1);
set.add(7);
//3.遍历
for(Integer obj : set) {
System.out.println(obj);
}
}
}
//输出结果:
1
3
5
7
截止这里一切都没有问题,我们继续往下看
入门案例二:
import java.util.Set;
import java.util.TreeSet;
class Person {
private String name;
private int age;
public Person() {}
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 "Person [name=" + name + ", age=" + age + "]";
}
}
public class Test_Person {
public static void main(String[] args) {
//1.实例化TreeSet
Set<Person> set = new TreeSet<>();
//2.添加元素
set.add(new Person("zs",21));
set.add(new Person("ls",20));
set.add(new Person("tom",19));
//3.遍历集合
for (Person person : set) {
System.out.println(person);
}
}
}
//程序运行,提示异常,具体如下:
Exception in thread "main" java.lang.ClassCastException:
包名.Person cannot be cast to java.lang.Comparable
at java.util.TreeMap.compare(TreeMap.java:1290)
at java.util.TreeMap.put(TreeMap.java:538)
at java.util.TreeSet.add(TreeSet.java:255)
at
包名.Test_Person.main(Test_Person.java:41)
看起来似乎没有问题啊
为什么会报错呢?
我们看一下报错原因:
ClassCastException:Person cannot be cast to java.lang.Comparable
原来是我们自定义的类没有实现 Comparable 接口
为什么会这样呢?
这就不得不引入新的知识点了:
排序规则
由于TreeSet是一个有序集合,那么想要有序必然要存在一定的规则
有两种方式:
1、自然排序
如果一个类实现了Comparable接口
并且重写了compareTo方法,那么这个类对象就可以进行排序了。
compareTo方法说明:
返回值:int
小于0:当前对象小,this.value < o.value
等于0:当前对象等于 this.value = o.value
大于0:当前对象大 this.value > o.value
调用者:当前对象的参数
实际参数:要比较的对象参数
调用方式:this.value.compareTo(o.value);
作用:
元素插入过程:先排序后去重
当TreeSet插入元素时会调用compareTo方法
如果方法返回-1,先插入this,o放后面
如果方法返回0,两个元素相等,只插入一个元素
如果方法返回1,先插入o,this放后面2、比较器排序
如果Student不是自定义类,而是第三方提供的类
我们不可以修改源码,就可以使用比较器排序
实现:
1、创建一个实现了 Comparator 接口的类
2、重写里面的 compare() 方法
3、创建一个 TreeSet(因为有序所以才排序)类对象时,将比较器对象作为参数传递给构造函数compare方法说明:
int result = compare(o1,o2);
——result的值大于0,表示升序
——result的值小于0,表示降序
——result的值等于0,表示元素相等,不能插入
由于返回值是int,处理非int类型时大致有三种情况:
①使用包装类的compare方法
// 比较double类型 public int compare(Student s1, Student s2) { return Double.compare(s1.getScore(), s2.getScore()); // 升序 // return Double.compare(s2.getScore(), s1.getScore()); // 降序 } // 比较float类型 public int compare(Student s1, Student s2) { return Float.compare(s1.getGPA(), s2.getGPA()); } // 比较long类型 public int compare(Student s1, Student s2) { return Long.compare(s1.getId(), s2.getId()); }
②手动转换比较结果
// double比较的手动实现 public int compare(Student s1, Student s2) { double diff = s1.getScore() - s2.getScore(); if (diff < 0) return -1; if (diff > 0) return 1; return 0; } // 但这种方式不推荐,因为可能存在精度问题
③其他类型
public class StudentComparator implements Comparator<Student> { @Override public int compare(Student s1, Student s2) { // 比较字符串 int nameCompare = s1.getName().compareTo(s2.getName()); if (nameCompare != 0) return nameCompare; // 比较double分数 int scoreCompare = Double.compare(s2.getScore(), s1.getScore()); // 降序 if (scoreCompare != 0) return scoreCompare; // 比较int年龄 return Integer.compare(s1.getAge(), s2.getAge()); } }
注意:
如果自然排序和比较器排序同时存在
优先使用比较器排序
案例展示:
public class Teacher implements Comparable<Teacher>{
private int id;
private String name;
private int age;
public Teacher() {
}
public Teacher(int id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
public int getId() {
return id;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
@Override
public String toString() {
return "Teacher [id=" + id + ", name=" + name + ", age=" + age + "]";
}
@Override
public int compareTo(Teacher o) {
int num = this.name.compareTo(o.name);
if(num == 0){
num = this.age - o.age;
}
if(num == 0){
num = this.id - o.id;
}
return num;
}
}
import java.util.Comparator;
import java.util.Set;
import java.util.TreeSet;
public class TestTeacher {
public static void main(String[] args) {
//自然排序,Comparable接口
Teacher t1 = new Teacher(001,"alan", 18);
Teacher t2 = new Teacher(002,"tom", 19);
Teacher t3 = new Teacher(003,"tom", 20);
Teacher t4 = new Teacher(004,"alan", 18);
Teacher t5 = new Teacher(005,"jack", 18);
Set<Teacher> set = new TreeSet<>();
System.out.println("自然排序:");
set.add(t1);
set.add(t2);
set.add(t3);
set.add(t4);
set.add(t5);
for(Teacher t : set){
System.out.println(t);
}
//比较器排序,Comparator接口
Comparator<Teacher> comparator = new Comparator<Teacher>() {
@Override
public int compare(Teacher t1, Teacher t2){
int num = t1.getName().compareTo(t2.getName());
if(num == 0){
num = t1.getAge() - t2.getAge();
}
if(num == 0){
num = t1.getId() - t2.getId();
}
return num;
}
};
Set<Teacher> set2 = new TreeSet<>(comparator);
System.out.println("比较器排序:");
set2.add(t1);
set2.add(t2);
set2.add(t3);
set2.add(t4);
set2.add(t5);
for(Teacher t : set2){
System.out.println(t);
}
}
}
3> LinkedHashSet
底层是链表 + 哈希表
父类 HashSet 所以具有父类唯一、高效的优点
接口 Set 保持元素的插入顺序,不会自动排序
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Set;
public class Test074_LinkedHashSet {
public static void main(String[] args) {
// 1.实例化LinkedHashSet
Set<String> set = new LinkedHashSet<>();
// 2.添加元素
set.add("bbb");
set.add("aaa");
set.add("abc");
set.add("bbc");
set.add("abc");
// 3.迭代器遍历
Iterator<String> it = set.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
}
}
//运行结果:
bbb
aaa
abc
bbc