注意:作者之前的Java基础没有打牢,有一些知识点没有记住,所以在学习中出现了许多零散的问题。现在特地写一篇笔记总结一下,所以有些知识点不是很齐全。
集合中各类名词的关系
Collection集合为单列集合。
集合存储数据类型的特点
单列集合
集合可以存储引用数据类型,无法存储基本数据类型。如果想要存储基本数据类型就要使用对应的包装类。
ArrayList的特点
//创建集合的对象
//泛型:限定集合中存储数据的类型,只支持引用数据类型
//泛型的格式:<数据类型>
ArrayList<String> list =new ArrayList<String>();
ArrayList<String> list =new ArrayList<>();
ArrayList
是 Java 集合框架中的一个重要类,它实现了 List
接口,用于存储元素的有序集合(也称为序列)。ArrayList
的特点包括:
- 动态数组:
ArrayList
内部使用动态数组来存储元素。这意味着当添加元素时,如果内部数组已满,ArrayList
会自动创建一个更大的数组,并将现有数组的内容复制到新数组中,然后添加新元素。- 由于使用了动态数组,
ArrayList
允许我们存储任意数量的元素(受限于可用内存)。- 有序性:
ArrayList
中的元素是有序的,它们按照被添加到列表中的顺序进行存储。我们可以通过索引(位置)来访问和修改元素。- 允许重复元素:
ArrayList
不检查元素的唯一性,因此它允许存储重复的元素。- 非同步:
ArrayList
不是线程安全的。如果多个线程同时访问和修改ArrayList
,可能会导致数据不一致或其他并发问题。如果需要在多线程环境中使用,应该考虑使用Collections.synchronizedList()
方法或CopyOnWriteArrayList
类。- 自动扩容:
- 当向
ArrayList
添加元素并超出其当前容量时,它会自动扩容。扩容通常涉及到创建一个新的更大的数组,并将现有数组的内容复制到新数组中。扩容的算法可能会导致额外的性能开销,因此在知道大致的元素数量时,可以通过构造方法或ensureCapacity()
方法预先设置容量。- 随机访问效率高:
- 由于
ArrayList
使用数组作为底层数据结构,因此它提供了基于索引的高效随机访问能力。通过索引访问元素的时间复杂度是 O(1)。- 基于索引的插入和删除效率低:
- 虽然
ArrayList
提供了基于索引的插入和删除方法(如add(int index, E element)
和remove(int index)
),但这些操作的时间复杂度是 O(n),因为可能需要移动数组中的其他元素。如果需要频繁地在列表中间进行插入和删除操作,LinkedList
可能是更好的选择。- 内存占用:
ArrayList
需要额外的空间来存储数组,这可能会导致它比某些其他集合类型(如LinkedList
)占用更多的内存。但是,由于ArrayList
提供了高效的随机访问能力,这种内存开销在某些情况下可能是值得的。
LinkedList的特点
LinkedList
是 Java 集合框架中的一部分,它实现了 List
接口,用于存储元素的列表。与 ArrayList
不同的是,LinkedList
底层基于双向链表实现,这给了它一些独特的特性和性能优势。以下是 LinkedList
的一些主要特点:
- 基于链表实现:
LinkedList
使用双向链表存储元素,这意味着每个元素都包含对前一个元素和后一个元素的引用。- 这种结构使得在列表的任何位置添加或删除元素都非常快,因为不需要移动其他元素。
- 添加和删除操作的高效性:
- 在
LinkedList
的头部或尾部添加或删除元素的时间复杂度是 O(1),因为可以直接访问这些位置并修改指针。- 而在列表的中间位置添加或删除元素时,需要遍历列表以找到正确的位置,但即使这样,其性能也通常优于
ArrayList
,因为ArrayList
需要移动所有后续元素。- 空间开销:
- 由于每个元素都需要存储对前一个元素和后一个元素的引用,因此
LinkedList
的空间开销略大于ArrayList
。- 然而,这种开销通常可以忽略不计,除非在非常紧凑的内存环境中工作。
- 迭代性能:
- 遍历
LinkedList
通常比遍历ArrayList
慢,因为链表中的元素在内存中可能不是连续存储的,这可能导致更多的缓存未命中和更慢的访问速度。- 但是,如果你需要频繁地在列表的头部或尾部添加或删除元素,并且很少遍历整个列表,那么
LinkedList
的性能可能会更好。- 线程不安全性:
- 与
ArrayList
一样,LinkedList
也不是线程安全的。如果多个线程同时修改列表,可能会导致数据不一致。- 如果你需要线程安全的列表,可以使用
Collections.synchronizedList()
方法包装LinkedList
,或者使用并发集合(如CopyOnWriteArrayList
或ConcurrentLinkedQueue
)。- 支持队列操作:
- 由于
LinkedList
实现了Deque
接口(双端队列),它支持从两端添加和删除元素的操作。- 这使得
LinkedList
可以用作队列(使用addFirst()
和removeFirst()
方法)或栈(使用push()
和pop()
方法)。- 容量和大小:
- 与
ArrayList
不同的是,LinkedList
没有初始容量或固定容量的概念。它可以根据需要动态增长。- 但是,请注意,链表中的元素在内存中可能是分散的,这可能导致内存碎片问题。
- 其他操作:
LinkedList
还提供了其他有用的方法,如getFirst()
,getLast()
,indexOf()
,lastIndexOf()
,element()
,offer()
,peek()
,poll()
等。
在选择使用 LinkedList
还是 ArrayList
时,你应该考虑你的具体需求,特别是你如何频繁地添加、删除和遍历列表中的元素。
Vector的特点
在Java中,Vector
集合的特点主要包括以下几点:
- 线程安全:
Vector
是线程安全的,这意味着多个线程可以同时访问和修改Vector
的内容。这是通过在每个方法上添加synchronized
关键字来实现的,但这也可能导致在高并发场景下性能下降。- 动态数组:与传统的数组不同,
Vector
可以根据需要动态地增加或减小其大小。当需要增加或减少元素的数量时,Vector
会自动调整其底层数组的大小。- 可以存储任意类型的元素:
Vector
可以存储任意类型的对象,包括基本类型的包装类对象。这使得Vector
具有很高的灵活性。- 有序性:
Vector
中的元素是按照插入顺序进行存储的,可以根据索引位置来访问和修改元素。这使得Vector
在需要保持元素顺序的场景下非常有用。- 访问速度快:由于
Vector
内部包含一个存储元素的数组,所有的元素都被存储在这个数组中,因此其访问速度非常快。只需要简单的访问数组的索引即可获取或修改元素。- 容量和大小:
Vector
的大小是指其包含的元素数量,而容量是指Vector
底层数组的大小。当Vector
中的元素数量超过其容量时,Vector
会自动扩容以容纳更多的元素。扩容时,Vector
的增长率通常为当前容量的100%,这意味着在大量数据的情况下,使用Vector
可能有一定的优势。- 使用场景:由于
Vector
的动态数组特性、线程安全特性以及高效的访问速度,它适用于许多应用场景,如需要在多线程环境下安全地访问和操作数据,或者需要动态调整数据大小并保持元素顺序的场景。
需要注意的是,虽然Vector
是线程安全的,但在高并发场景下,由于其同步机制可能导致性能下降。因此,在不需要线程安全性的情况下,通常建议使用ArrayList
来提高性能。
HashSet的特点
Java中的HashSet
集合具有以下几个主要特点:
不包含重复元素:
HashSet
基于HashMap
实现,它不允许存储重复的元素。当尝试向HashSet
中添加一个已经存在的元素时,该操作不会有任何效果,即元素不会被添加进去,并且add
方法会返回false
。无序性:
HashSet
不保证元素的迭代顺序与插入顺序相同。这是因为HashSet
内部使用哈希表来存储元素,而哈希表并不保证元素的顺序。非同步:
HashSet
不是线程安全的,即多个线程可以同时访问和修改一个HashSet
对象。在多线程环境下,如果需要对HashSet
进行同步访问,则需要使用额外的同步机制,如Collections.synchronizedSet()
方法或ConcurrentHashSet
(Java标准库中没有直接提供ConcurrentHashSet
,但可以使用ConcurrentHashMap
的键集合来模拟)。元素存储的唯一性依赖于对象的hashCode()和equals()方法:在判断两个元素是否相等时,
HashSet
首先会比较它们的哈希值(通过调用hashCode()
方法),如果哈希值相同,则会进一步调用equals()
方法来判断它们是否相等。因此,要正确使用HashSet
存储元素,必须保证存储的元素正确重写了hashCode()
和equals()
方法。快速查找:由于
HashSet
基于哈希表实现,因此它提供了非常快的查找速度。无论是通过contains()
方法检查元素是否存在,还是通过迭代器遍历元素,通常都能在常数时间内完成。支持null元素:
HashSet
允许存储一个null元素。但是,由于HashSet
不允许存储重复元素,因此只能存储一个null元素。非泛型集合:虽然
HashSet
在实际使用时通常会与泛型一起使用(如HashSet<String>
),但HashSet
本身并不是泛型集合。它是Java 1.2中引入的,而泛型是在Java 5中引入的。不过,从Java 5开始,推荐使用泛型化的HashSet
来避免类型转换和类型不安全的操作。
TreeSet的特点
Java中的TreeSet
集合具有以下几个主要特点:
- 元素有序:
TreeSet
中的元素是有序的,默认按照元素的自然顺序进行排序。如果元素实现了Comparable
接口,则按照元素的自然顺序进行排序;如果没有实现Comparable
接口,则必须提供一个Comparator
接口的实现来指定排序顺序。也可以在创建TreeSet
时传入自定义的比较器来进行排序。- 无重复元素:
TreeSet
不允许存储重复的元素。当尝试添加重复元素时,新元素不会被添加到集合中。- 基于红黑树实现:
TreeSet
的底层数据结构是红黑树,这是一种自平衡的二叉搜索树。红黑树的自平衡性使得TreeSet
能够实现快速的查找、插入和删除操作。因此,TreeSet
在需要有序集合且频繁进行查找操作的场景中非常适用。- 查询效率高:由于底层红黑树的特性,
TreeSet
的查询效率非常高。可以在O(log n)的时间复杂度内完成查找操作,其中n是TreeSet
中元素的个数。- 非线程安全:
TreeSet
不是线程安全的,如果多个线程同时访问一个TreeSet
,并且至少有一个线程对其进行了修改,则必须通过外部同步手段来保证线程安全。- 支持迭代器:
TreeSet
支持迭代器,可以通过迭代器遍历集合中的元素。
总的来说,TreeSet
是一种基于红黑树实现的有序集合,适用于需要有序集合且频繁进行查找操作的场景。同时,由于它的非线程安全特性,在使用时需要注意线程同步问题。
双列集合(待补充)
集合的创建
单列集合
Array List集合的创建
package 集合;
import java.util.ArrayList;
import java.util.Collection;
public class CollectionTest {
public static void main(String[] args){
ArrayList<String> c=new ArrayList<>(); <--
//添加元素
c.add("zangng");
c.add("hua");
String i=c.get(0);//取出特定元素
System.out.println(i);
System.out.println("c的集合元素个数:"+c.size());
c.remove("zangng");//删除特定元素
System.out.println("c的集合元素个数:"+c.size());
}
}
--------------------------------------------------------------
package 集合;
import java.util.ArrayList;
import java.util.List;
public class CollectionTest2 {
public static void main(String[] args){
List<String> c=new ArrayList<>(); <--
//添加元素
c.add("zangng");
c.add("hua");
String i=c.get(0);//取出特定元素
System.out.println(i);
System.out.println("c的集合元素个数:"+c.size());
c.remove("zangng");//删除特定元素
System.out.println("c的集合元素个数:"+c.size());
}
}
两种方式的区别和联系
在Java中,List<String> list = new ArrayList<String>();
和 ArrayList<String> c = new ArrayList<>();
这两行代码在功能上是等效的,但它们在类型引用和代码简洁性方面有所不同。
- 类型引用:
-
List<String> list = new ArrayList<String>();
:这里,我们创建了一个ArrayList
的实例,但将其引用赋值给一个List
类型的变量list
。这意味着我们只能使用List
接口中定义的方法,而不能直接使用ArrayList
类中可能提供的任何特定方法(除非我们稍后将list
强制转换回ArrayList
类型)。这是一种好的编程实践,因为它提高了代码的可读性和可维护性,同时也使得代码更加灵活,因为我们可以在不修改代码其他部分的情况下,将list
引用指向ArrayList
的其他实现(如LinkedList
)。 -
ArrayList<String> c = new ArrayList<>();
:这里,我们创建了一个ArrayList
的实例,并直接将其引用赋值给一个ArrayList
类型的变量c
。这意味着我们可以直接使用ArrayList
类中定义的所有方法,但这也限制了我们在不修改代码其他部分的情况下,将c
引用指向List
的其他实现。
-
- 代码简洁性(从Java 7开始):
- 从Java 7开始,引入了菱形操作符(diamond operator)
<>
,用于简化泛型代码的编写。因此,new ArrayList<String>()
可以简写为new ArrayList<>()
。所以,List<String> list = new ArrayList<>();
是List<String> list = new ArrayList<String>();
的简写形式,并且它们是完全等效的。
- 从Java 7开始,引入了菱形操作符(diamond operator)
在实际编程中,推荐的做法是使用接口作为类型引用(如List<String>
),而不是具体的实现类(如ArrayList<String>
),除非我们有特定的理由需要直接使用实现类的方法。这样做的好处是提高了代码的灵活性和可维护性。如果我们确定代码只会使用ArrayList
并且不会更改实现,那么使用ArrayList<String>
作为类型引用也是可以的,但通常这不是一个好的做法。
拓展(仅供参考)
除了List<String> list = new ArrayList<String>();和ArrayList<String> c=new ArrayList<>();还有其他方式实现Array List吗?
在Java中,虽然直接实例化ArrayList
是创建ArrayList
实例的最常见方式,但还有一些其他间接或变种的方式来实现类似的功能。以下是一些可能的方法:
使用Java 8的Stream API:
虽然这不是直接创建ArrayList
的方式,但你可以使用Stream API来生成一个ArrayList
。
List<String> list = Stream.of("a", "b", "c").collect(Collectors.toList());
这里,Stream.of("a", "b", "c")
创建了一个包含字符串的Stream,然后collect(Collectors.toList())
将流中的元素收集到一个新的List
中,这通常是一个ArrayList
的实例(尽管不是明确地指定为ArrayList
类型)。
使用Arrays.asList():
虽然Arrays.asList()
返回的是一个固定大小的列表(它实际上是Arrays
类的内部类的一个实例,并不是ArrayList
),但你可以将这个列表传递给ArrayList
的构造器来创建一个新的可变列表。
List<String> fixedList = Arrays.asList("a", "b", "c");
ArrayList<String> arrayList = new ArrayList<>(fixedList);
使用CopyOnWriteArrayList:CopyOnWriteArrayList
是ArrayList
的一个线程安全变体。它在修改操作(如添加、设置)时创建底层数组的新副本,因此读取操作(如get
)是无锁的,从而提高了并发性能。但是,请注意,由于它在修改时复制底层数组,因此它可能不适合在需要频繁修改的大型列表上使用。
List<String> threadSafeList = new CopyOnWriteArrayList<>(Arrays.asList("a", "b", "c"));
使用Collections.synchronizedList():
你可以使用Collections.synchronizedList()
方法来包装任何实现了List
接口的集合(包括ArrayList
),以提供线程安全的列表。但是,这种同步是基于方法的,并且可能不如CopyOnWriteArrayList
在并发读取时高效。
List<String> syncList = Collections.synchronizedList(new ArrayList<>(Arrays.asList("a", "b", "c")));
使用第三方库:
除了Java标准库中的集合类之外,还有一些第三方库提供了额外的集合实现,这些实现可能具有不同的性能特征或额外的功能。例如,Google Guava库提供了许多有用的集合类,包括ImmutableList
(不可变列表)、Multimap
(键值对可以重复的映射)等。
使用Java 9的List.of():
从Java 9开始,你可以使用List.of()
方法来创建一个不可变的列表,这在你只需要一个固定大小的列表并且不需要修改它时很有用。
List<String> immutableList = List.of("a", "b", "c");
但请注意,List.of()返回的列表是不可变的,因此它不能替代需要可变性的ArrayList。
同类型其他集合创建
LinkedList集合的创建
使用LinkedList和List的区别其实在上文提过,与ArrayList有异曲同工之妙。
//创建方法一
LinkedList<String> c= new LinkedList<>();
//创建方法二
List<String> c= new LinkedList<>();
Vector集合的创建
//创建方法一
Vector<String> vector = new Vector<>();
//创建方法二
List<String> c=new Vector<>();
HashSet集合的创建
//创建方法一
HashSet<String> c = new HashSet<>();
//创建方法二
Set<String> c = new HashSet<>();
TreeSet集合的创建
//创建方法一
TreeSet<String> c = new TreeSet<>();
//创建方法二
Set<String> c = new TreeSet<>();
双列集合(待补充)
集合的遍历格式(待补充)
Lambda表达式
Lambda表达式(Lambda Expression)是Java 8及以后版本中引入的一个新特性,它允许我们以更简洁的方式表示匿名函数(即没有名称的函数)。Lambda表达式提供了一种新的语法来定义函数式接口(Functional Interface,即只包含一个抽象方法的接口)的实例。
Lambda表达式的基本语法如下:
parameters
:这是Lambda表达式的参数列表。参数的类型可以省略(即类型推断),但如果Lambda表达式的参数超过一个或者参数的类型需要明确指定时,则必须加上括号。->
:这是Lambda操作符,用于分隔参数列表和Lambda体。expression
或{ statements; }
:这是Lambda体,可以是单个表达式(其值就是Lambda表达式的返回值)或者一个语句块(如果有多条语句)。
Lambda表达式常用于与函数式接口一起使用,作为这些接口的实例。Java 8在java.util.function
包中引入了许多新的函数式接口,如Function
、Predicate
、Consumer
等,它们都可以使用Lambda表达式来实例化。
下面是一些Lambda表达式的例子:
package Lambda表达式;
//无参
public class Test01 {
public static void main(String[] args){
//使用lambda表达式实现接口
Test test1=()->{
System.out.println("test");
};
test1.test();
}
}
//lambda表达式,只能实现函数式接口。
//如果说,⼀个接口中,要求实现类必须实现的抽象方法,有且只有⼀个!这样的接口,就是函数式接口。
//@FunctionalInterface是⼀个注解,用在接口之前,判断这个接口是否是⼀个函数式接口。
// 如果是函数式接口,没有任何问题。如果不是函数式接口,则会报错。功能类似于 @Override。
@FunctionalInterface
interface Test{
//有且只有一个实现类必须要实现的抽象方法,所以是函数式接口
public void test();
}
Lambda表达式极大地简化了匿名内部类的编写,使代码更加简洁易读。同时,它也为Java带来了函数式编程的能力,使得Java能够更好地支持并发编程和流式数据处理等现代编程范式。