一、Java中的集合框架
1.为什么使用集合框架
2.集合框架的继承关系(简略)
graph BT
ArrayList-->List
LinkedList-->List
HashSet-->Set
TreeSet-->Set
List-->Collection
Set-->Collection
graph BT
HashMap-->Map
TreeMap-->Map
List是可重复且有序的,Set是不可重复且无序的
二、List
1.List类型的特点
Java中的List是一种接口类型,它定义了一种有序的集合数据结构。List中的元素按照插入的顺序存储,并且可以通过索引来访问元素。以下是List类型的主要特点:
- 有序集合:List中的元素按照插入的顺序进行排序,因此元素的顺序是有意义的。可以通过索引来访问元素,索引从0开始。
- 可以包含重复元素:List允许包含重复元素,即多个元素可以具有相同的值。
- 动态性:List是动态的,可以在运行时添加、删除或修改元素。这使得List非常灵活,可以适应程序运行时的变化需求。
- 易于使用:List提供了一组简单而强大的方法来操作数据,例如add、get、remove等。这使得开发人员可以轻松地使用List来处理数据。
- 可扩展性:List接口具有良好的扩展性,可以通过实现List接口来创建自己的List实现。这使得List可以根据需求进行扩展和定制,以满足特定的应用程序需求。
2.ArrayList案例和LinkedList案例
ArrayList:
public class ArrayListTest {
public static void main(String[] args) {
//声明的时候最好采用多态的形式,也就是以父接口的形式去声明
//增加元素
List list=new ArrayList();
list.add("西瓜");
list.add("苹果");
list.add("草莓");
list.add("西瓜");
//修改元素
list.set(1,"面包");//下标从0开始,此处将‘苹果’修改成了‘面包’
//删除元素
list.remove(1);//remove可以直接通过对象删除,也可以通过下标删除
//查询元素
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
}
LinkedList:
public class LinkedListTest {
public static void main(String[] args) {
List list=new LinkedList();
list.add("西瓜");
list.add("苹果");
list.add("草莓");
list.add("西瓜");
//修改元素
list.set(1,"面包");
//删除元素
list.remove(1);
//查询元素
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
}
3.ArrayList底层原理和LinkedList底层原理
-
ArrayList底层是由==数组==实现的
其默认长度为10
将elementData这个存储ArrayList元素的数组作为缓冲区
通过ensureCapacity()方法进行动态扩容
- LinkedList底层是由双向链表实现的
其初始化时不需要申请内存空间
4.ArrayList和LinkedList如何选择
ArrayList可以根据索引直接访问元素,因此可以快速随机访问元素。在添加或删除元素时,需要移动后面的所有元素,因此这种操作的效率较低。如果需要高效的随机访问元素,并且添加或删除元素的频率较低,那么应该选择ArrayList。
LinkedList无法进行随机访问元素,但是添加或删除元素时只需要修改指针,因此它的效率相对较高。如果需要高效的添加或删除元素,并且对访问元素的顺序并没有特别的要求,那么应该选择LinkedList。
在开发时,主要的业务场景是追加和遍历,所以使用ArrayList的频率更高。
5.List存放复杂的数据类型
先定义一个Dog复杂数据类型
public class Dog {
private String name;
private String type;
//此处忽略了全参数构造方法和get,set方法
@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +
", type='" + type + '\'' +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Dog dog = (Dog) o;
return Objects.equals(name, dog.name) && Objects.equals(type, dog.type);
}
@Override
public int hashCode() {
return Objects.hash(name, type);
}
}
将其存入ArrayList中
public class ArrayListTest {
public static void main(String[] args) {
List list=new ArrayList();
list.add(new Dog("a","a"));
list.add(new Dog("b","b"));
list.add(new Dog("c","c"));
System.out.println("共有"+list.size()+"条狗");
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
//判断list中是否包含某个对象,或者移除对象都需要重写equals方法
System.out.println(list.contains(new Dog("a", "a")));
System.out.println(list.remove(new Dog("a", "a")));
}
}
contain包含,remove移除,传入的对象都需要重写equals()方法,否则操作失败返回false
6.LinkedList独有的方法
因为LinkedList是链表实现,所以有一些方法针对其实现
| 方法名 | 说明 |
|---|---|
| void addFirst(Object o) | 在列表首部添加元素 |
| void addLast(Object o) | 在列表尾部添加元素 |
| Object getFirst() | 返回列表的第一个元素 |
| Object getLast() | 返回列表的最后一个元素 |
| Object removeFirst() | 删除并返回列表中的第一个元素 |
| Object removeLast() | 删除并返回列表中的最后一个元素 |
三、Set
1.Set类型的特点
- 无序的:Set中的元素没有特定的顺序。添加到Set的元素不会保持原来的顺序。
- 无重复的元素:Set的一个重要特性是它不允许重复元素。如果尝试添加一个已经存在的元素,Set将不会接受它。
- 线程不安全:Set接口的实现类(例如HashSet和TreeSet)不是线程安全的。如果你在多线程环境中使用,需要额外的同步措施。
- 不允许null元素:Set不允许包含null元素。
- 没有父/子关系:与List不同,Set没有子列表的概念。
- 基于Map实现:大多数Set的实现(如HashSet和LinkedHashSet)是基于Map实现的。这意味着添加和删除操作通常具有O(1)的复杂度,而查找操作具有O(n)的复杂度(在最佳情况下,如在TreeSet中,查找操作具有O(log n)的复杂度)。
- 用于存储单例:由于Set不允许重复元素,所以它是存储单例的理想结构。
- 支持泛型:从Java 5开始,Set接口的实现类(如HashSet和TreeSet)支持泛型,这使得在编译时检查类型错误成为可能。
2.HashSet案例
HashSet
public class HashSetTest {
public static void main(String[] args) {
Set set = new HashSet();
set.add("苹果");
set.add("苹果");
set.add("香蕉");
set.add("椰子");
set.add("桃子");
//因为set是无序的,因为没有下标,所以只能使用增强型for循环
for (Object o : set) {
System.out.println(o);
}
}
}
/**
运行结果:不可重复,且无序的
桃子
苹果
香蕉
椰子
*/
3.Set的去重原理
先创建一个复杂类型Dog
public class Dog {
private String name;
private String type;
//省略了get和set方法以及构造函数
@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +
", type='" + type + '\'' +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Dog dog = (Dog) o;
return Objects.equals(name, dog.name) && Objects.equals(type, dog.type);
}
}
HashSet
public class HashSetTest {
public static void main(String[] args) {
Set set = new HashSet();
set.add(new Dog("a","a"));
//与第一条相同
set.add(new Dog("a","a"));
set.add(new Dog("b","b"));
set.add(new Dog("c","c"));
set.add(new Dog("d","d"));
set.add(new Dog("e","e"));
//因为set是无序的,因为没有下标,所以只能使用增强型for循环
for (Object o : set) {
System.out.println((Dog)o);
}
}
}
/*
运行结果:
Dog{name='b', type='b'}
Dog{name='e', type='e'}
Dog{name='a', type='a'}
Dog{name='d', type='d'}
Dog{name='a', type='a'}
Dog{name='c', type='c'}
**/
此时,复杂类型并没有被去重!!!
所以,此时需要在Dog类中重写hashCode方法
@Override
public int hashCode() {
super.hashCode();
return Objects.hash(name, type);
}
再执行,成功去重
Dog{name='a', type='a'}
Dog{name='b', type='b'}
Dog{name='c', type='c'}
Dog{name='d', type='d'}
Dog{name='e', type='e'}
Set去重的原理主要基于对象的hashCode()和equals()方法。当添加元素到Set时,首先会计算元素的hashCode,然后在Set内部查找具有相同hashCode的元素。如果找到了具有相同hashCode的元素,那么就会调用equals()方法来比较这个元素和Set内部存储的元素是否相同。如果equals()返回true,那么就会忽略新添加的元素,实现去重,简而言之,Set去重一定要重写hashCode()和equals()方法。
4.TreeSet算法依赖于一个比较接口
代码案例
public class TreeSetTest {
public static void main(String[] args) {
Set set = new TreeSet();
set.add("a");
set.add("a");
set.add("b");
set.add("c");
set.add("d");
set.add("e");
for (Object o:set){
System.out.println(o);
}
}
}
/**
添加字符串已经去重
a
b
c
d
e
*/
当添加的是对象时
public class TreeSetTest {
public static void main(String[] args) {
Set set = new TreeSet();
set.add(new Dog("a","a"));
set.add(new Dog("a","a"));
set.add(new Dog("b","b"));
set.add(new Dog("c","c"));
set.add(new Dog("d","d"));
set.add(new Dog("e","e"));
for (Object o : set) {
System.out.println((Dog)o);
}
}
}
报错
出现这个问题是因为,TreeSet在添加遍历复杂类型时,必须实现一个比较接口,Comparable,同时重写compareTo(T o)方法。
public class Dog implements Comparable<Dog>{
private String name;
private String type;
//省略了get和set方法已经全参构造函数
@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +
", type='" + type + '\'' +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Dog dog = (Dog) o;
return Objects.equals(name, dog.name) && Objects.equals(type, dog.type);
}
@Override
public int hashCode() {
return Objects.hash(name, type);
}
@Override
public int compareTo(Dog o) {
return name.compareTo(o.name)+type.compareTo(o.type);
}
}
此时再运行,没有报错
Dog{name='a', type='a'}
Dog{name='b', type='b'}
Dog{name='c', type='c'}
Dog{name='d', type='d'}
Dog{name='e', type='e'}
为什么直接添加字符串不需要实现Comparable接口
原因:String类型中已经自动实现了Comparable了。
四、Map
1.Map类型的特点
Java中的Map是一种接口类型,它定义了一种将键映射到值的数据结构。Map中的元素是通过键值对来存储和访问的。以下是Map类型的主要特点:
- 键的唯一性:在Map中,每个键只能对应一个值。如果尝试使用相同的键插入多个值,那么后续的插入操作将覆盖先前的值。
- 键和值的存储:Map中的键和值可以是任何类型的对象,包括自定义的对象类型。键和值的存储是相互独立的,即键的类型与值的类型可以是不同的类型。
- 键的有序性:在Java中,Map的内部实现有多种方式,如HashMap、LinkedHashMap等。不同的实现方式对键的存储顺序可能会有所不同。例如,HashMap不保证键的顺序,而LinkedHashMap则按照键的插入顺序进行排序。
- Map的遍历:可以通过迭代器(Iterator)来遍历Map中的所有元素。遍历时,元素的顺序可能与插入顺序不同,具体取决于Map的实现方式。
- Map的操作:Map提供了一些常用的操作方法,如put(key, value)用于插入或更新元素,get(key)用于根据键获取对应的值,remove(key)用于删除指定键对应的元素等。
- Map的实现类:Java提供了多个Map的实现类,如HashMap、LinkedHashMap、TreeMap等。这些实现类具有不同的特点和用途,可以根据实际需求选择合适的实现方式。
2.HashMap基本实现原理
- 数据存储:HashMap内部采用数组+链表(Java 8后为数组+树)的方式存储数据。数组是HashMap的基础,每个数组元素就是一个Node对象,包含三个部分:key、value和next指针。
- 散列映射:当我们将键值对存入HashMap时,首先会对键进行散列(hash)操作,得到一个散列值,这个散列值对应数组的某个位置,然后将该键值对存入该位置的Node中。
- 解决冲突:由于不同的键可能会得到相同的散列值,因此可能会产生冲突。HashMap通过链表的方式解决这个问题。当两个或多个键得到相同的散列值时,它们会被存放在同一个链表中。在查询时,如果根据key得到的散列值相同,就会在对应的链表中按照next指针顺序查找。
- 树的构建(Java 8及以后版本): 当链表的长度大于一定阈值(默认为8)时,会将链表转换为红黑树(一种自平衡的二叉搜索树)。树的节点数总是大于等于链表的节点数,所以查找、插入、删除在树上的操作时间复杂度是O(log n)。
- 树的合并(Java 8及以后版本): 当红黑树的节点数小于等于6时,会将红黑树转回为链表。
3.HashMap案例
public class HashMapTest {
public static void main(String[] args) {
//双列,键值结构
//key不可重复,value可重复
Map hashMap = new HashMap<>();
hashMap.put("a", "注意看这个");
hashMap.put("b", "桃子");
hashMap.put("c", "梨");
hashMap.put("d", "芒果");
hashMap.put("a", "被修改了");//修改
System.out.println("长度为:"+hashMap.size());
//拿到hashMap中所有的key
Set set = hashMap.keySet();
for (Object o : set) {
System.out.println(o+":"+hashMap.get(o));
}
}
}
/**
长度为:4
a:被修改了
b:桃子
c:梨
d:芒果
*/
因为Set是无序的,取到的key也是无序的,所以打印出来也是无序的
4.Map常用方法
代码示例:
public class HashMapTest {
public static void main(String[] args) {
Map hashMap = new HashMap<>();
hashMap.put("a", "注意看这个");
hashMap.put("b", "桃子");
hashMap.put("c", "梨");
hashMap.put("d", "芒果");
hashMap.put("a", "被修改了");
System.out.println("长度为:"+hashMap.size());//获取长度
System.out.println(hashMap.get("a"));//通过key取value
System.out.println(hashMap.containsKey("b"));//查询hashMap中是否存在某个键
System.out.println(hashMap.remove("c"));//移除键,返回对应的value
System.out.println(hashMap.keySet());//返回一个存有key的set
System.out.println(hashMap.values());//返回一个存有value的collection
hashMap.clear();//清空hashMap
System.out.println("长度为:"+hashMap.size());
}
}
/**
长度为:4
被修改了
true
梨
[a, b, d]
[被修改了, 桃子, 芒果]
长度为:0
*/
5.泛型在集合中的应用
为什么使用泛型
在上述代码例子中,key和value可以添加任意数据类型的值,会出现一些类型不匹配和强制类型转换的问题。
使用泛型还可以提高代码的灵活性和可重用性。假设我们有一个程序需要处理不同类型的数据,如果使用Object类型的键和值,则需要使用强制类型转换来获取具体类型的对象。而使用泛型,我们可以在定义List,Set,Map集合时指定具体的类型,使得代码更加清晰和易于维护。
代码示例(以List举例,未使用泛型):
public class GenericTest {
public static void main(String[] args) {
List list=new ArrayList();
list.add(new Dog("a","a"));
list.add(1);
list.add("test");
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
}
/**
Dog{name='a', type='a'}
1
test
*/
代码可读性极低,当数据量极大时,需要取list中Dog类型的数据进行操作,需要先使用到instanceof关键字判断是否为Dog类型,然后再进行操作
泛型入门
public class GenericTest<T> {
T a;
public void show(T a) {
System.out.println(a);
}
public static void main(String[] args) {
GenericTest objectGenericTest = new GenericTest();
objectGenericTest.show("a");
objectGenericTest.show(123);
}
}
使用泛型可以传入不同数据类型的数据,也可以指定传入的数据类型。
指定传入的数据类型
public class GenericTest<T> {
T a;
public void show(T a) {
System.out.println(a);
}
public static void main(String[] args) {
GenericTest<String> objectGenericTest = new GenericTest<String>();
objectGenericTest.show("a");
}
}
指定传入的数据类型不能是基础数据类型,比如指定传入整型,就只能使用int类型的包装类型Integer
在Map中使用泛型
在代码中插入一条非String类型的数据
所以,在使用泛型时申明了传入的数据类型,便只能传入对应的类型。
提示:尖括号可以在一些版本下简写
Map<String, String> hashMap = new HashMap<String, String>();//后面的那个<>可以省略
Map<String, String> hashMap = new HashMap();//可以写成这样,但是只能是jdk6以上的版本才能省略
补充
迭代器Iterator
**增强型for循环(语法糖)**的底层都是靠迭代器实现,其原理大致为:
- 对于数组:增强for循环会隐式地创建一个ArrayIterator,该ArrayIterator负责遍历数组并返回数组的每个元素。对于基本类型的数组,这个ArrayIterator是类型特定的,即每个类型都会有一个对应的ArrayIterator类。
- 对于集合(Collection):增强for循环会隐式地创建一个迭代器,该迭代器会遍历集合并返回集合的每个元素。对于实现了Iterable接口的集合类(如List,Set等),增强for循环可以直接使用Iterable接口的iterator方法获取一个迭代器。
graph LR
HashSet-->|extends|AbstractSet-->|extends|AbstractCollection-->|implements|Collection-->|extends|Iterable
Iterator使用案例
public class IteratorTest {
public static void main(String[] args) {
Set<Integer> set = new HashSet<Integer>();
set.add(1);set.add(2);set.add(3);set.add(4);
Iterator<Integer> iterator = set.iterator();
while (iterator.hasNext()) {//判断是否存在下一个值
System.out.println(iterator.next());//获取迭代器中的下一个元素
}
}
}
/*
1
2
3
4
**/
Collections集合框架工具类
public class CollectionsUtilTest {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
for (int i = 0; i <= 54; i++) {
list.add(i);//生成了一个0到54的列表
}
System.out.println(list);//列表初始状态
Collections.addAll(list,55,56);//追加元素
System.out.println(list);
Collections.reverse(list);//将整个列表反向
System.out.println(list);
Collections.shuffle(list);//类似洗牌的操作,将整个列表打乱
System.out.println(list);
}
}
本节主要介绍了三个工具方法,主要用于列表中的操作,更多方法可以查看官方文档,搜索Collections。