JAVA随记——集合篇

发布于:2024-05-09 ⋅ 阅读:(34) ⋅ 点赞:(0)

注意:作者之前的Java基础没有打牢,有一些知识点没有记住,所以在学习中出现了许多零散的问题。现在特地写一篇笔记总结一下,所以有些知识点不是很齐全。

 集合中各类名词的关系

Collection集合为单列集合。

集合存储数据类型的特点

单列集合

集合可以存储引用数据类型,无法存储基本数据类型。如果想要存储基本数据类型就要使用对应的包装类。

ArrayList的特点

//创建集合的对象
//泛型:限定集合中存储数据的类型,只支持引用数据类型
//泛型的格式:<数据类型>
ArrayList<String> list =new ArrayList<String>();
ArrayList<String> list =new ArrayList<>();

ArrayList 是 Java 集合框架中的一个重要类,它实现了 List 接口,用于存储元素的有序集合(也称为序列)。ArrayList 的特点包括:

  1. 动态数组
    • ArrayList 内部使用动态数组来存储元素。这意味着当添加元素时,如果内部数组已满,ArrayList 会自动创建一个更大的数组,并将现有数组的内容复制到新数组中,然后添加新元素。
    • 由于使用了动态数组,ArrayList 允许我们存储任意数量的元素(受限于可用内存)。
  2. 有序性
    • ArrayList 中的元素是有序的,它们按照被添加到列表中的顺序进行存储。我们可以通过索引(位置)来访问和修改元素。
  3. 允许重复元素
    • ArrayList 不检查元素的唯一性,因此它允许存储重复的元素。
  4. 非同步
    • ArrayList 不是线程安全的。如果多个线程同时访问和修改 ArrayList,可能会导致数据不一致或其他并发问题。如果需要在多线程环境中使用,应该考虑使用 Collections.synchronizedList() 方法或 CopyOnWriteArrayList 类。
  5. 自动扩容
    • 当向 ArrayList 添加元素并超出其当前容量时,它会自动扩容。扩容通常涉及到创建一个新的更大的数组,并将现有数组的内容复制到新数组中。扩容的算法可能会导致额外的性能开销,因此在知道大致的元素数量时,可以通过构造方法或 ensureCapacity() 方法预先设置容量。
  6. 随机访问效率高
    • 由于 ArrayList 使用数组作为底层数据结构,因此它提供了基于索引的高效随机访问能力。通过索引访问元素的时间复杂度是 O(1)。
  7. 基于索引的插入和删除效率低
    • 虽然 ArrayList 提供了基于索引的插入和删除方法(如 add(int index, E element) 和 remove(int index)),但这些操作的时间复杂度是 O(n),因为可能需要移动数组中的其他元素。如果需要频繁地在列表中间进行插入和删除操作,LinkedList 可能是更好的选择。
  8. 内存占用
    • ArrayList 需要额外的空间来存储数组,这可能会导致它比某些其他集合类型(如 LinkedList)占用更多的内存。但是,由于 ArrayList 提供了高效的随机访问能力,这种内存开销在某些情况下可能是值得的。

LinkedList的特点

LinkedList 是 Java 集合框架中的一部分,它实现了 List 接口,用于存储元素的列表。与 ArrayList 不同的是,LinkedList 底层基于双向链表实现,这给了它一些独特的特性和性能优势。以下是 LinkedList 的一些主要特点:

  1. 基于链表实现
    • LinkedList 使用双向链表存储元素,这意味着每个元素都包含对前一个元素和后一个元素的引用。
    • 这种结构使得在列表的任何位置添加或删除元素都非常快,因为不需要移动其他元素。
  2. 添加和删除操作的高效性
    • 在 LinkedList 的头部或尾部添加或删除元素的时间复杂度是 O(1),因为可以直接访问这些位置并修改指针。
    • 而在列表的中间位置添加或删除元素时,需要遍历列表以找到正确的位置,但即使这样,其性能也通常优于 ArrayList,因为 ArrayList 需要移动所有后续元素。
  3. 空间开销
    • 由于每个元素都需要存储对前一个元素和后一个元素的引用,因此 LinkedList 的空间开销略大于 ArrayList
    • 然而,这种开销通常可以忽略不计,除非在非常紧凑的内存环境中工作。
  4. 迭代性能
    • 遍历 LinkedList 通常比遍历 ArrayList 慢,因为链表中的元素在内存中可能不是连续存储的,这可能导致更多的缓存未命中和更慢的访问速度。
    • 但是,如果你需要频繁地在列表的头部或尾部添加或删除元素,并且很少遍历整个列表,那么 LinkedList 的性能可能会更好。
  5. 线程不安全性
    • 与 ArrayList 一样,LinkedList 也不是线程安全的。如果多个线程同时修改列表,可能会导致数据不一致。
    • 如果你需要线程安全的列表,可以使用 Collections.synchronizedList() 方法包装 LinkedList,或者使用并发集合(如 CopyOnWriteArrayList 或 ConcurrentLinkedQueue)。
  6. 支持队列操作
    • 由于 LinkedList 实现了 Deque 接口(双端队列),它支持从两端添加和删除元素的操作。
    • 这使得 LinkedList 可以用作队列(使用 addFirst() 和 removeFirst() 方法)或栈(使用 push() 和 pop() 方法)。
  7. 容量和大小
    • 与 ArrayList 不同的是,LinkedList 没有初始容量或固定容量的概念。它可以根据需要动态增长。
    • 但是,请注意,链表中的元素在内存中可能是分散的,这可能导致内存碎片问题。
  8. 其他操作
    • LinkedList 还提供了其他有用的方法,如 getFirst()getLast()indexOf()lastIndexOf()element()offer()peek()poll() 等。

在选择使用 LinkedList 还是 ArrayList 时,你应该考虑你的具体需求,特别是你如何频繁地添加、删除和遍历列表中的元素。

Vector的特点

在Java中,Vector集合的特点主要包括以下几点:

  1. 线程安全Vector是线程安全的,这意味着多个线程可以同时访问和修改Vector的内容。这是通过在每个方法上添加synchronized关键字来实现的,但这也可能导致在高并发场景下性能下降。
  2. 动态数组:与传统的数组不同,Vector可以根据需要动态地增加或减小其大小。当需要增加或减少元素的数量时,Vector会自动调整其底层数组的大小。
  3. 可以存储任意类型的元素Vector可以存储任意类型的对象,包括基本类型的包装类对象。这使得Vector具有很高的灵活性。
  4. 有序性Vector中的元素是按照插入顺序进行存储的,可以根据索引位置来访问和修改元素。这使得Vector在需要保持元素顺序的场景下非常有用。
  5. 访问速度快:由于Vector内部包含一个存储元素的数组,所有的元素都被存储在这个数组中,因此其访问速度非常快。只需要简单的访问数组的索引即可获取或修改元素。
  6. 容量和大小Vector的大小是指其包含的元素数量,而容量是指Vector底层数组的大小。当Vector中的元素数量超过其容量时,Vector会自动扩容以容纳更多的元素。扩容时,Vector的增长率通常为当前容量的100%,这意味着在大量数据的情况下,使用Vector可能有一定的优势。
  7. 使用场景:由于Vector的动态数组特性、线程安全特性以及高效的访问速度,它适用于许多应用场景,如需要在多线程环境下安全地访问和操作数据,或者需要动态调整数据大小并保持元素顺序的场景。

需要注意的是,虽然Vector是线程安全的,但在高并发场景下,由于其同步机制可能导致性能下降。因此,在不需要线程安全性的情况下,通常建议使用ArrayList来提高性能。

HashSet的特点

Java中的HashSet集合具有以下几个主要特点:

  1. 不包含重复元素HashSet基于HashMap实现,它不允许存储重复的元素。当尝试向HashSet中添加一个已经存在的元素时,该操作不会有任何效果,即元素不会被添加进去,并且add方法会返回false

  2. 无序性HashSet不保证元素的迭代顺序与插入顺序相同。这是因为HashSet内部使用哈希表来存储元素,而哈希表并不保证元素的顺序。

  3. 非同步HashSet不是线程安全的,即多个线程可以同时访问和修改一个HashSet对象。在多线程环境下,如果需要对HashSet进行同步访问,则需要使用额外的同步机制,如Collections.synchronizedSet()方法或ConcurrentHashSet(Java标准库中没有直接提供ConcurrentHashSet,但可以使用ConcurrentHashMap的键集合来模拟)。

  4. 元素存储的唯一性依赖于对象的hashCode()和equals()方法:在判断两个元素是否相等时,HashSet首先会比较它们的哈希值(通过调用hashCode()方法),如果哈希值相同,则会进一步调用equals()方法来判断它们是否相等。因此,要正确使用HashSet存储元素,必须保证存储的元素正确重写了hashCode()equals()方法。

  5. 快速查找:由于HashSet基于哈希表实现,因此它提供了非常快的查找速度。无论是通过contains()方法检查元素是否存在,还是通过迭代器遍历元素,通常都能在常数时间内完成。

  6. 支持null元素HashSet允许存储一个null元素。但是,由于HashSet不允许存储重复元素,因此只能存储一个null元素。

  7. 非泛型集合:虽然HashSet在实际使用时通常会与泛型一起使用(如HashSet<String>),但HashSet本身并不是泛型集合。它是Java 1.2中引入的,而泛型是在Java 5中引入的。不过,从Java 5开始,推荐使用泛型化的HashSet来避免类型转换和类型不安全的操作。

TreeSet的特点

Java中的TreeSet集合具有以下几个主要特点:

  1. 元素有序TreeSet中的元素是有序的,默认按照元素的自然顺序进行排序。如果元素实现了Comparable接口,则按照元素的自然顺序进行排序;如果没有实现Comparable接口,则必须提供一个Comparator接口的实现来指定排序顺序。也可以在创建TreeSet时传入自定义的比较器来进行排序。
  2. 无重复元素TreeSet不允许存储重复的元素。当尝试添加重复元素时,新元素不会被添加到集合中。
  3. 基于红黑树实现TreeSet的底层数据结构是红黑树,这是一种自平衡的二叉搜索树。红黑树的自平衡性使得TreeSet能够实现快速的查找、插入和删除操作。因此,TreeSet在需要有序集合且频繁进行查找操作的场景中非常适用。
  4. 查询效率高:由于底层红黑树的特性,TreeSet的查询效率非常高。可以在O(log n)的时间复杂度内完成查找操作,其中n是TreeSet中元素的个数。
  5. 非线程安全TreeSet不是线程安全的,如果多个线程同时访问一个TreeSet,并且至少有一个线程对其进行了修改,则必须通过外部同步手段来保证线程安全。
  6. 支持迭代器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<>(); 这两行代码在功能上是等效的,但它们在类型引用和代码简洁性方面有所不同。

  1. 类型引用:
    • 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的其他实现。

  2. 代码简洁性(从Java 7开始):
    • 从Java 7开始,引入了菱形操作符(diamond operator)<>,用于简化泛型代码的编写。因此,new ArrayList<String>() 可以简写为 new ArrayList<>()。所以,List<String> list = new ArrayList<>(); 是 List<String> list = new ArrayList<String>(); 的简写形式,并且它们是完全等效的。

在实际编程中,推荐的做法是使用接口作为类型引用(如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:
CopyOnWriteArrayListArrayList的一个线程安全变体。它在修改操作(如添加、设置)时创建底层数组的新副本,因此读取操作(如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包中引入了许多新的函数式接口,如FunctionPredicateConsumer等,它们都可以使用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能够更好地支持并发编程和流式数据处理等现代编程范式。