专栏首页MasiMaro 的技术博文Java 学习笔记(10)——容器

Java 学习笔记(10)——容器

之前学习了java中从语法到常用类的部分。在编程中有这样一类需求,就是要保存批量的相同数据类型。针对这种需求一般都是使用容器来存储。之前说过Java中的数组,但是数组不能改变长度。Java中提供了另一种存储方式,就是用容器类来处理这种需要动态添加或者删除元素的情况

概述

Java中最常见的容器有一维和多维。单维容器主要是一个节点上存储一个数据。比如列表和Set。而多维是一个节点有多个数据,例如Map,每个节点上有键和值。 单维容器的上层接口是Collection,它根据存储的元素是否为线性又分为两大类 List与Set。它们根据实现不同,List又分为ArrayList和LinkedList;Set下面主要的实现类有TreeSet、HashSet。

Collection 接口

Collection 是单列容器的最上层的抽象接口,它里面定义了所有单列容器都共有的一些方法:

  • boolean add(E e):向容器中添加元素
  • void clear(): 清空容器
  • boolean contains(Object o): 判断容器中是否存在对应元素
  • boolean isEmpty(): 容器是否为空
  • boolean remove(Object o): 移除指定元素
  • <T> T[] toArray(T[] a): 转化为指定类型的数组

List

list是Collection 中的一个有序容器,它里面存储的元素都是按照一定顺序排序的,可以使用索引进行遍历。允许元素重复出现,它的实现中有 ArrayList和 LinkedList

  • ArrayList 底层是一个可变长度的数组,它具有数组的查询快,增删慢的特点
  • LinkedList 底层是一个链表,它具有链表的增删快而查询慢的特点

Set

Set集合是Collection下的另一个抽象结构,Set类似于数学概念上的集合,不关心元素的顺序,不能存储重复元素。

  • TreeSet是一颗树,它拥有树形结构的相关特定
  • HashSet: 为了加快查询速度,它的底层是一个hash表和链表。但是从JDK1.8以后,为了进一步加快具有相同hash值的元素的查询,底层改为hash表 + 链表 + 红黑树的结构。相同hash值的元素个数不超过8个的采用链表存储,超过8个之后采用红黑树存储。 在存储元素的时候,首先计算它的hash值,根据hash值,在数组中查找,如果没有,则在数组对应位置存储hash值,并在数组对应位置添加元素的节点。如果有,则先判断对应位置是否有相同的元素,如果有则直接抛弃否则在数组对应位置下方的链表或者红黑树中添加节点。

从上面的描述看,想要在HashSet中添加元素,需要首先计算hash值,在判断集合中是否存在元素。这样在存储自定义类型的元素的时候,需要保证类能够正确计算hash值以及进行类型的相等性判断。因此要重写类的hashCodeequals 方法。 例如下面的例子

class Person{
    private String name;
    private int age;

    Person(){

    } 

    Person(String name, int age){
        this.name = name;
        this.age = age;
    }

    public int getAge(){
        return this.age;
    }

    public String getName(){
        return this.name;
    }

    public void setAge(int age){
        this.age = age;
    }

    public void setName(String name){
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age &&
                Objects.equals(name, person.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(this.name, this.age);
    }
}

上面说到HashSet是无序的结构,如果我们想要使用hashSet,但是又想它有序,该怎么办?在Set中提供了另一个实现,LinkedHashMap。它的底层是一个Hash表和一个链表,Hash表用来存储真正的数据,而链表用来存储元素的顺序,这样就结合了二者的优先。

Map

Map是一个双列的容器,一个节点存储了两个值,一个是元素的键,另一个是值。其中Key 和 Value既可以是相同类型的值,也可以是不同类型的值。Key和Value是一一对应的关系。一个key只能对应一个值,但是多个key可以指向同一个value,有点像数学中函数的自变量和值的关系。

Map常用的实现类有: HashMap和LinkedHashMap。

常用的方法有:

  • void clear(): 清空集合
  • boolean containsKey(Object key): map中是否包含对应的键
  • V get(Object key): 根据键返回对应的值
  • V put(K key, V value): 添加键值对
  • boolean isEmpty(): 集合是否为空
  • int size(): 包含键值对的个数

遍历

针对列表类型的,元素顺序固定,我们可以使用循环依据索引进行遍历,比如

for(int i = 0; i < list.size(); i++){
    String s = list.get(i);
}

而对于Set这种不关心元素的顺序的集合来说,不能再使用索引了。针对单列集合,有一个迭代器接口,使用迭代器可以实现遍历

迭代器

迭代器可以理解为指向集合中某一个元素的指针。使用迭代器可以操作元素本身,也可以根据当前元素寻找到下一个元素,它的常用方法有:

  • boolean hasNext() : 当前迭代器指向的位置是否有下一个元素
  • E next(): 获取下一个元素并返回。调用这个方法后,迭代器指向的位置发生改变

使用迭代器的一般步骤如下:

  1. 使用集合的 iterator() 返回一个迭代器
  2. 循环调用迭代器的 hasNext方法,判断集合中是否还有元素需要遍历
  3. 使用 next方法,找到迭代器指向的下一个元素
//假设set是一个 HashSet<String>集合
Iterator<String> it = set.iterator();
while(it.hasNext()){
    Stirng s = it.next();
}

Map遍历

索引和迭代器的方式只能遍历单列集合,像Map这样的多列集合不能使用上述方式,它有额外的方法,主要有两种方式

  1. 获取key的一个集合,遍历key集合并通过get方法获取value
  2. 获取键值对组成的一个集合,遍历这个新集合来得到键值对的值

针对第一种方法,Map中有一个 keySet() 方法。这个方法会获取到所有的key值并保存将这些值保存为一个新的Set返回,我们只要遍历这个Set并调用 Map的get方法即可获取到对应的Value, 例如:

// 假设map 是一个 HashMap<String, String> 集合
Set<String> kSet = map.keySet();
Iterator<String> key = kSet.iterator();
while(it.hasNext()){
    String key = it.next();
    String value = map.get(key);
}

针对第二种方法,可以先调用 Map的 entrySet() 获取一个Entry结构的Set集合。Entry 中保存了一个键和它对应的值。使用结构中的 getKey()getValue() 分别获取key和value。这个结构是定义在Map中的内部类,因此在使用的时候需要使用Map这个类名调用

// 假设map 是一个 HashMap<String, String> 集合
Set<Map.Entry<String,String>> entry = map.entrySet();
Iterator<Map.Entry<String, String>> it = entry.iterator();
while(it.hasNext()){
    Map.Entry<String, String> me = it.next();
    String key = me.getKey();
    String value = me.getValue();
}

for each 循环

在上述遍历的代码中,不管是使用for或者while都显得比较麻烦,我们能像 Python 等脚本语言那样,直接在 for 中使用迭代吗?从JDK1.5 以后引入了for each写法,使Java能够直接使用for迭代,而不用手工使用迭代器来进行迭代。

for (T t: set); 

上述是它的简单写法。 例如我们对遍历Set的写法进行简化

//假设set是一个 HashSet<String>集合
for(String s: set){
    //TODO:do some thing
}

我们说使用 for each写法主要是为了简化迭代的写法,它在底层仍然采用的是迭代器的方式来遍历,针对向Map这样无法直接使用迭代的结构来说,自然无法使用这种简化的写法,针对Map来说需要使用上述的两种遍历方式中的一种,先转化为可迭代的结构,然后使用for each循环

// 假设map 是一个 HashMap<String, String> 集合
Set<Map.Entry<String, String>> set = map.entrySet();
for(Map.Entry<String, String> entry: set){
    String key = entry.getKey();
    String value = entry.getValue();
    System.out.println(key + "-->" + value);
}

泛型

在上述的集合中,我们已经使用了泛型。

泛型与C++ 中的模板基本类似,都是为了重复使用代码而产生的一种语法。由于这些集合在创建,增删改查上代码基本类似,只是事先不知道要存储的数据的类型。如果没有泛型,我们需要将所有类型对应的这些结构的代码都重复写一遍。有了泛型我们就能更加专注于算法的实现,而不用考虑具体的数据类型。 在定义泛型的时候,只需要使用 <>中包含表示泛型的字母即可。常见的泛型有:

  • T 表示Type
  • E 表示 Element

<> 中可以使用任意标识符来表示泛型,只要符合Java的命名规则即可。使用 T 或者 E 只是为了方便而已,比如下面的例子

public static <Element> void print(Element e){
    System.out.println(e);
}

当然也可以使用Object 对象来实现泛型的重用代码的功效,在对元素进行操作的时候主要使用java的多态来实现。但是使用多态的一个缺点是无法使用元素对象的特有方法。

泛型的使用

泛型可以在类、接口、方法中使用

在定义类时定义的泛型可以在类的任意位置使用

class DataCollection<T>{
    private T data;
    public T getData(){
        return this.data;
    }

    public void SetData(T data){
        this.data = data;
    }
}

在定义类的时候定义的泛型在创建对象的时候指定具体的类型.

也可以在定义接口的时候定义泛型

public interface DataCollection<T>{
    public abstract T getData();
    public abstract void setData(T data);
}

定义接口时定义的泛型可以在定义实现类的时候指定泛型,或者在创建实现类的对象时指定泛型

public class StringDataCollectionImpl implements DataCollection<String>{
    private String data;
    public String getData(){
        return this.data;
    }

    public void SetData(String data){
        this.data = data;
    }
}

public interface DataCollection<T> implements DataCollection<T>{
    private T data;
    public T getData(){
        return this.data;
    }

    public void SetData(T data){
        this.data = data;
    }
}

除了在定义类和接口时使用外,还可以在定义方法的时候使用,针对这种情况,不需要显示的指定使用哪种类型,由于接收返回数据和传入参数的时候已经知道了

public static <Element> Element print(Element e){
    System.out.println(e);
    return e;
}
String s = print("hello world");

泛型的通配符

在使用通配符的时候可能有这样的需求:我想要使用泛型,但是不希望它传入任意类型的值,我只想要处理继承自某一个类的类型,就比如说我只想保存那些实现了某个接口的类。我们当然可以将数据类型定义为某个接口,但是由于多态的这一个缺陷,实现起来总不是那么完美。这个时候可以使用泛型的通配符。 泛型中使用 ? 作为统配符。在通配符中可以使用 super 或者 extends 表示泛型必须是某个类型的父类或者是某个类型的实现类

class Fruit{

}

class Apple extends Fruit{

}

class Bananal extends Fruit{

}

static void putFruit(<? extends Fruit> data){

}

上述代码中 putFruit 函数中只允许 传递 Fruit 类的子类或者它本身作为参数。

当然也可以使用 <? super T> 表示只能取 T类型的父类或者T类型本身。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 程序隐藏到任务栏的实现

    我们在使用软件的时候,有的软件允许最小化到任务栏,然后双击任务栏的图标时又会显示出来,这篇文章主要说明如何实现这种功能; 实现这种功能主要分为两步,一是将程...

    Masimaro
  • Java 学习笔记(15)——反射

    Java中的类文件最终会被编译为.class 文件,也就是Java字节码。这个字节码中会存储Java 类的相关信息。在JVM执行这些代码时首先根据 java 命...

    Masimaro
  • 对话框伸缩功能的实现

    对话框的伸缩功能是指当触发某一操作时只显示部分对话框的内容,再次触发时显示全部的对话框范围,就比如画图软件上的选择颜色对话框,我们可以使用系统预定义的颜色,也可...

    Masimaro
  • [Swagger] Swagger Codegen 高效开发客户端对接服务端代码

    需要注意的是,自动生成的是客户端代码,请求/swagger-resource/user/info时,可以通过自动生成的代理类UserResourceContro...

    架构探险之道
  • java之泛型

    泛型是JDK1.5新加入的,解决数据类型的安全性问题,其主要原理是在类声明时通过一个标识表示类中某个属性的类型或者是某个方法的返回值及参数类型。这样在类声明或实...

    绝命生
  • Java中泛型的介绍和使用

    我们都知道Java里的集合(list,map....)是可以存放任意对象的,只要把对象存储集合后,那么这时他们都会被提升成Object类型。当我们在取出每一个对...

    框架师
  • 快速学习-代码生成器搭建环境

    cwl_java
  • 重构:以Java POI 导出EXCEL为例2

    上一篇博文已经将一些对象抽象成成员变量以及将一些代码块提炼成函数。这一节将会继续重构原有的代码,将一些函数抽象成类,增加成员变量,将传入的参数合成类等等。

    云枭
  • Kotlin学习笔记(七)-泛型

    这节我们说下Kotlin的泛型。首先默认大家对Java泛型有个基本的认识,如果 不熟悉Java的泛型,可以阅读文章,或是看下Java《Java核心技术卷一基础知...

    g小志
  • Android | UDP的C/S通信实战案例(简易聊天室)

    凌川江雪

扫码关注云+社区

领取腾讯云代金券