java 之容器

简介: 在Java中,我们想要保存对象可以使用很多种手段。我们之前了解过的数组就是其中之一。但是数组具有固定的尺寸,而通常来说,程序总是在运行时根据条件来创建对象,我们无法预知将要创建对象的个数以及类型,所以Java推出了容器类来解决这一问题。Java的容器类分为List,Set,Queue和Map。我们也称它们为集合类(Collection)。Java使用泛型来实现容器类,例如我们要使用顺序表这

在Java中,我们想要保存对象可以使用很多种手段。我们之前了解过的数组就是其中之一。但是数组具有固定的尺寸,而通常来说,程序总是在运行时根据条件来创建对象,我们无法预知将要创建对象的个数以及类型,所以Java推出了容器类来解决这一问题。

Java的容器类分为List,Set,QueueMap。我们也称它们为集合类(Collection)。

Java使用泛型来实现容器类,例如我们要使用顺序表这一数据结构,Java提供了ArrayList和LinkedList两种实现类,ArrayList的实现就是基于数组的。比如我们要存储一组用户,在Java8之前的版本,我们就可以这样声明对象:List<User> users = new ArrayList<User>();。然后通过add方法来添加变量。

Java7及Java8的容器

如果你是一个喜欢新事物,也不妨尝试下Java7,它可以对泛型的目标类型进行推断。我们就可以这样声明这个对象List<User> users = new ArrayList<>();

Java7中,编译器会根据变量声明时的泛型类型自动推断出实例化所用的泛型类型。但是它在创建泛型实例时的类型推断是有限制的:只有构造器的参数化类型在上下文中被显著的声明了,才可以使用类型推断,否则不行。比如:

List<String> list = new ArrayList<>();
list.add("A");// 由于addAll期望获得Collection<? extends String>类型的参数,因此下面的语句无法通过
list.addAll(new ArrayList<>());

而在Java8中,它支持两种泛型的目标类型推断:

1.支持通过方法上下文推断泛型目标类型

2.支持在方法调用链路当中,泛型类型推断传递到最后一个方法

上述程序可以更改如下:

//通过方法赋值的目标参数来自动推断泛型的类型
List<String> list = List.nil();
//通过前面方法参数类型推断泛型的类型
List.cons(42, List.nil());

Java容器的基本概念

Java容器类库是用来保存对象的,他有两种不同的概念:

  1. Collection。独立元素的序列,这些元素都服从一条或多条规则。ListSet以及Queue都是Collection的一种,List必须按照顺序保存元素,而Set不能有重复元素,Queue需要按照排队规则来确定对象的顺序。

  2. MapMap是键值对类型,允许用户通过键来查找对象。ArrayList允许使用数字来查找值,Hash表允许我们使用另一个对象来查找某个对象。

尽管存在这两种概念,我们在工程中,大部分代码还是和接口打交道。Collection接口概括了序列的概念,即存放一组对象的方式。ArrayList,HashSet等具体类均实现了Collection接口或Collection接口的子接口(List接口和Set接口等)。

Collection接口的定义如下:

public interface Collection<E> extends Iterable<E> {

    int size();

    boolean isEmpty();

    boolean contains(Object o);

    Iterator<E> iterator();

    Object[] toArray();

    <T> T[] toArray(T[] a);

    boolean add(E e);

    boolean remove(Object o);

    boolean containsAll(Collection<?> c);

    boolean addAll(Collection<? extends E> c);

    boolean removeAll(Collection<?> c);

    boolean retainAll(Collection<?> c);

    void clear();

    boolean equals(Object o);

    int hashCode();
}

我们可以看出Collection接口实际上继承了Iterable接口,实现这个接口的类可以使用迭代器以及foreach语法进行遍历。

size, isEmpty, contains, iterator, toArray, add, remove, containAll, addAll, removeAll, clear方法分别表示获取这个Collection类型的对象的元素个数,是否为空,是否包含某个元素,获取迭代器,转换为数组,增加元素,删除元素,某个Collection对象是否为它的子集以及进行取差集和清空操作。

除了上述成员方法,java.utils包中的ArraysCollections类中还提供了很多实用的方法,如:

  • Arrays.asList()方法可以接受数组或逗号分隔的元素列表,并将其转化为一个List对象。
  • Collections.addAll()方法接受一个Collection对象和一个数组或以逗号分隔的列表将其加入到集合当中。
  • 等等

我们可以这样使用:

//使用asList方法生成list
List<String> keywords = Arrays.asList("hello", "thank", "you");
//我们要将其他元素加入到keywords容器中
Collections.addAll(keywords, "very", "much");

使用asList()方法输出产生的对象需要注意一些问题,因为在这种情况下,它的底层表示仍然是数组,因此我们是不能够该表它的尺寸的。这时使用adddelete方法可能会引发改变数组尺寸的尝试,会在运行时得到Unsupported Operation错误。

如果要使用可以改变尺寸的List,我推荐大家在获取到asList()方法的输出后,再构造一个ArrayList。

迭代器

从之前的Collection接口中可以看出,任何容器类,都可以以某种方式插入、获取和删除元素。add()作为最基本的插入元素方法而get()则是基本取元素的方法。

但是如果我们仅仅使用get和add方法来进行元素操作,如果将一个类的方法实现了,如果想要将相同的代码用在其他容器类中就会遇到问题,那么我们如何解决这一问题呢?

在这里我们就引入了面向对象的设计模式迭代器模式。迭代器是一个对象,它的工作是遍历并选择序列中的对象。客户端不需要知道序列的底层架构。

Java的Iterator的定义如下:

public interface Iterator<E> {

    boolean hasNext();

    E next();

    void remove();
}

我们可以使用:

  1. next()方法来获取序列的下一个元素。

  2. hasNext()检查序列中是否还有元素。

  3. 使用remove()将迭代器新近返回的元素删除。比如我们要遍历一个容器:

List<String> keywords = new ArrayList<>();
keywords.add("hello");
keywords.add(0, "thank");
Iterator<String> iterator = keywords.iterator();
while (iterator.hasNext()) {
    System.out.println(iterator.next());
}

List<String> keywords = new ArrayList<>();
keywords.add("hello");
keywords.add(0, "thank");
for (String keyword : keywords) {
    System.out.println(iterator.next());
}

Iterator还有一些功能更为强大的子类型,我会在下文予以介绍。在接下来的几节我会依次和大家介绍Java容器类中的几种接口。

List

List可以将元素维护在特定的序列中。List接口继承于Collection接口,并在此基础上添加了大量的方法,使得我们可以在List中间进行元素的插入和移动。

List有两种类型分别为:

  1. ArrayList,擅长随机访问元素,但是插入、删除元素较慢
  2. LinkedList,擅长插入、删除和移动元素,但是随机访问元素性能较低。

提示

学过数据结构的朋友们应该都知道,ArrayList是我们平时所使用的数组,而LinkedList就是链表。

数组的存储在内存空间中是连续的。所以在底层,我们可以通过每个元素所占的内存大小以及偏移量计算出每个元素所在的起始地址。但是在删除、插入元素时,由于需要保证数据存储位置的连续性,我们需要对它周围的元素进行搬移,而周围元素的搬移又会引起后续其他元素的搬移需求,所以最终所导致的移动操作很多。

而链表在内存中并不是连续存储的。它是一种逻辑顺序结构,每个链表存储的对象,都会存储下一个元素以及上一个元素的引用,通过引用来进行迭代。在删除、移动和插入时,我们不需要对元素的实际位置进行搬移,仅仅需要改变引用就可以了。但是由于它是逻辑上的顺序表,我们不能够静态的计算它的位置,只能一个一个的寻找,所以它的随机存取性能较低。

List接口的实例化对象可以使用Collection的所有方法:

List<String> keywords = new ArrayList<>();
List<String> oldKeywords = new LinkedList<>();
keywords.add("hello");
keywords.add(0, "thank");
oldKeywords.add("you");
oldKeywords.add(0, "very");
keywords.addAll(oldKeywords);
keywords.addAll(2, oldKeywords);
keywords.remove(3);
keywords.removeAll(oldKeywords);
List<String> subKeywords = keywords.subList(0, 1);
keywords.clear();

在使用时,我们会发现ArrayList类型的对象和LinkedList类型对象性能的不同。其中需要注意的是倒数第二行我们使用的subList函数是List接口独有的,它可以获取顺序表的一部分生成一个新的List。

ListIterator

ListIterator是更为强大的Iterator的子类型,但是它仅仅针对List的类进行访问。ListIterator可以进行双向移动、获取迭代器所处元素的前后元素的索引,还可以使用set()方法替换它访问过的最后一个元素。如:

List<String> keywords = new ArrayList<>();
keywords.add("hello");
keywords.add(0, "thank");
ListIterator<String> iterator = keywords.listIterator();
while (iterator.hasPrevious()) {
    System.out.println(iterator.previous());
}
while (iterator.hasNext()) {
    iterator.next();
    iterator.set("you");
}

Stack

Stack 实现了栈数据结构,它是一种LIFO(后进先出)的容器。也就是我们先放进栈的元素,在使用时会先获取到最后放入的元素。

Stack<String> stack = new Stack<>();
stack.push("hello");
stack.push("thank");
while (!stack.empty()) {
    System.out.println(stack.pop());
}

Set

Set是一种不保存重复元素的数据结构。如果我们将多个相同元素放入Set中,它仅仅会保存一个。使用Set很适合进行查找操作,Java中提供了一个HashSet类,它的查找速度很快,适合用作快速查找。

在使用时与其他Collection的使用类似:

Set<String> keywords = new HashSet<>();
keywords.add("hello");
keywords.add("thank");
keywords.add("u");
keywords.add("thank");
keywords.add("u");
keywords.add("very");
keywords.add("much");
System.out.println(keywords);

我们在set中加入了一系列的词汇,其中有一些重复词汇,但是在实际输出时我们会发现,并不存在那么多的元素,而仅仅打印不重复元素。

Set有多种实现:

  1. HashSet,使用了散列方式进行存储。
  2. TreeSet,将元素存储在红黑数当中。它会对集合元素进行排序。
  3. LinkedHashSet,使用链表和哈希表来实现Set。

提示

具体的实现我们可以在数据结构的教程中深入了解,在这里我只与大家分享该如何在工程中选取数据结构。比如我们需要获取一个排好序的数列集合。我们就可以使用TreeSet,插入元素后,元素就会按照顺序存储。我们可以很方便的插入或删除元素同时保证排序质量。如果我们不需要排序,只需要保证插入和查找效率,那我们就可以仅仅使用HashSet来进行工作,我们可以很方便的通过它来测试元素的归属性,以及进行一系列的集合操作。

Map

Map可以将一个对象映射到另一个对象。在工程上,它是十分重要的数据结构。比如我们有一系列用户分组对象它保存了用户分组的信息,我们经常需要通过用户分组对象获取这个分组的所有用户。如果我们仅仅通过List进行存储,在查找时的工作量是很大的。因为我们需要从头开始遍历List,判断每个元素是否属于这一分组,但是引入Map后就简单许多了,我们可以将一个对象映射到另一个对象上,所以可以这样实现:

Map<Department, List<User>> departmentUsersMap = new HashMap<>();
departmentUsersMap.put(department1, users1);
departmentUsersMap.put(department2, users2);
//在获取时
List<User> departmentUser = departmentUsersMap.get(department);

提示

这次我们第一次用到了多维的实现,Map中嵌套List,事实上容器的嵌套层次是可以很深的。我们甚至将在Map中的List再嵌套一个Set。但是我们使用何种数据结构,要取决于我们程序的需求,我们数据结构的组合选择需要最大程度的满足我们的需求并尽可能地提高程序的效率。

Map数据结构除了上述映射获取功能以外,还可以获取键、值或键值对的集合,分别使用keySet, value以及entrySet。比如我们要遍历map:

Map<Department, List<User>> departmentUsersMap = new HashMap<>();
departmentUsersMap.put(department1, users1);
departmentUsersMap.put(department2, users2);
for (Map.Entry<String, String> departmentEntry : departmentUsersMap.entrySet()) {
    System.out.println(String.format("key:%s value:%s", departmentEntry.getKey().toString(), departmentEntry.getValue()
            .toString()));
}



目录
相关文章
|
6月前
|
Java 虚拟化 容器
(Java)Java里JFrame窗体的基本操作(容器布局篇-1)
容器 容器,我的理解是可以包容其他东西的玩意。它可以是一个盒子,可以是一个虚拟化的物品,可只要能包裹住其他存在质体的东西,那么都可以称作是容器。例如:JPanel组件和JScollPane组件两者都是容器也是组件。 既然有容器,那么容器中的布局就必不可少了。不然不规矩的摆放物品,人类看不习惯,我也看不习惯 ???? 本篇内容,将说明java JFrame窗体里容器中几类布局。 说明:所有在JFrame窗体里的容器布局都会使用setLayout()方法,采用的布局参数都将放进这个方法里 绝对布局 调用窗体容器
193 1
|
10月前
|
存储 缓存 安全
Java 集合容器常见面试题及详细解析
本文全面解析Java集合框架,涵盖基础概念、常见接口与类的特点及区别、底层数据结构、线程安全等内容。通过实例讲解List(如ArrayList、LinkedList)、Set(如HashSet、TreeSet)、Map(如HashMap、TreeMap)等核心组件,帮助读者深入理解集合容器的使用场景与性能优化。适合准备面试或提升开发技能的开发者阅读。
173 0
|
Kubernetes Cloud Native Java
云原生之旅:从容器到微服务的演进之路Java 内存管理:垃圾收集器与性能调优
【8月更文挑战第30天】在数字化时代的浪潮中,企业如何乘风破浪?云原生技术提供了一个强有力的桨。本文将带你从容器技术的基石出发,探索微服务架构的奥秘,最终实现在云端自由翱翔的梦想。我们将一起见证代码如何转化为业务的翅膀,让你的应用在云海中高飞。
|
10月前
|
缓存 Java API
Java 集合容器实操技巧与案例详解
本教程基于Java 8+新特性和现代开发实践,深入讲解Java集合容器的实操技巧。通过具体场景演示Stream API数据处理、ConcurrentHashMap并发控制、LinkedHashMap实现LRU缓存、TreeSet自定义排序等高级特性。同时涵盖computeIfAbsent优化操作、EnumMap专用集合使用、集合统计与运算(交集、并集、差集)等内容。代码示例丰富,助力掌握高效编程方法。[点击获取完整代码](https://pan.quark.cn/s/14fcf913bae6)。
188 0
|
Java Linux Maven
java依赖冲突解决问题之容器加载依赖jar包如何解决
java依赖冲突解决问题之容器加载依赖jar包如何解决
|
存储 安全 算法
Java容器及其常用方法汇总
Java Collections框架提供了丰富的接口和实现类,用于管理和操作集合数据。
297 2
Java容器及其常用方法汇总
|
监控 Java 中间件
8G的容器Java堆才4G怎么就OOM了?
本文记录最近一例Java应用OOM问题的排查过程,希望可以给遇到类似问题的同学提供参考。
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
消息中间件 NoSQL Kafka
Flink-10 Flink Java 3分钟上手 Docker容器化部署 JobManager TaskManager Kafka Redis Dockerfile docker-compose
Flink-10 Flink Java 3分钟上手 Docker容器化部署 JobManager TaskManager Kafka Redis Dockerfile docker-compose
510 4
|
Kubernetes Cloud Native 流计算
Flink-12 Flink Java 3分钟上手 Kubernetes云原生下的Flink集群 Rancher Stateful Set yaml详细 扩容缩容部署 Docker容器编排
Flink-12 Flink Java 3分钟上手 Kubernetes云原生下的Flink集群 Rancher Stateful Set yaml详细 扩容缩容部署 Docker容器编排
455 3