Java EE 第四篇 核心类库(一)集合

一、泛型

泛型,即“参数化类型”。就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定 义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。

(1)使用:

1、泛型类:

public class ClassName<T>{
    private T data;
    public T getData() {
        return data;
    }
    public void setData(T data) {
        this.data = data;
    }
}

2、泛型接口:

public interface IntercaceName<T>{
    T getData();
}

//实现接口时,可以选择指定泛型类型,也可以选择不指定, 如下:
//指定类型:
public class Interface1 implements IntercaceName<String> {
    private String text;
    
    @Override
    public String getData() {
        return text;
    }
}

//不指定类型:
public class Interface1<T> implements IntercaceName<T> {
    private T data;
    @Override
    public T getData() {
        return data;
    }
}

3、泛型方法:

private static <T> T function(T a, T b) {}

(2)泛型限制类型:

在使用泛型时,可以指定泛型的限定区域
例如: 必须是某某类的子类或 某某接口的实现类,格式:
<T extends 类或接口1 & 接口2>

类型通配符是使用?代替方法具体的类型实参。
1 <? extends Parent> 指定了泛型类型的上界
2 <? super Child> 指定了泛型类型的下界
3 <?> 指定了没有限制的泛型类型

(3)好处:

1、 提高代码复用率
2、 泛型中的类型在使用时指定,不需要强制类型转换(类型安全,编译器会检查类型)

注意:

  • 在编译之后程序会采取去泛型化的措施。
  • 也就是说Java中的泛型,只在编译阶段有效。
  • 在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,并且在对象进入和离开方法的边界处添加 类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。

二、Java SE 类集

Java 中为了方便用户操作各个数据结构, 所以引入了类集的概念,有时候就可以把类集称为 java 对数据结构的实现。类集中最大的几个操作接口:Collection、Map、Iterator,这三个接口为以后要使用的最重点的接口。 所有的类集操作的接口或类都在 java.util 包中。

1、Collection接口

Collection 接口是在整个 Java 类集中保存单值的最大操作父接口,里面每次操作的时候都只能保存一个对象的数据。在开发中不会直接使用 Collection 接口。而使用其操作的子接口:List、Set。

//定义
public interface Collection<E> extends Iterable<E>
方法:

2、List接口

在整个集合中 List 是 Collection 的子接口,里面的所有内容都是允许重复的。

//定义
public interface List<E> extends Collection<E>
方法:此接口对于 Collection 接口来讲有如下的扩充方法:
常用的实现类有如下几个:

使用频率:ArrayList(95%)、Vector(4%)、LinkedList(1%)

ArrayList、Vector采用动态数组实现,前者线程不安全,后者安全。LinkedList采用链表实现。

2.1、ArrayList

ArrayList是List接口的子类,此类的定义如下:

public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, Serializable

此类继承了AbstractList 类。AbstractList是List接口的子类。AbstractList是个抽象类,适配器设计模式。ArrayLIst增加删除比较慢,查找比较快。创建时必须使用引用类型或包装类构造。

ArrayList();//创建初始容量为10的空列表。
ArrayList(int initialCapacity);
ArrayList(Collection<? extends E> e)

注意,注意对空列表进行空间为10的赋值是在空列表添加元素调用add方法的时候,内部扩容算法将新长度赋值为10,add方法的返回值永远为true。

2.2、Vector

定义:
public class Vector<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, Serializable

Vector 属于 Java 元老级的操作类,是最早的提供了动态对象数组的操作类,在 JDK 1.0 的时候就已经推出了此类的使用,只是后来在 JDK 1.2 之后引入了 Java 类集合框架。但是为了照顾很多已经习惯于使用 Vector 的用户,所以在 JDK 1.2 之后将 Vector 类进行了升级了,让其多实现了一个 List 接口,这样才将这个类继续保留了下来。

Vector为可增长对象数组,区别与ArrayList在于线程安全,增加慢,查找快。

构造方法:
Vector();
Vector(int initialCapacity);
Vector(int initialCapacity, int capacityIncrement); //额外赋值扩容增量,无参构造方法默认增量为0,此时的扩容方法为翻一番。
Vector(Collection<? extends E> e)
与ArrayLIst的区别:

2.3、LinkedList

使用场景很少,定义:
public class LinkedList<E> extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, Serializable

此类继承了 AbstractList,所以是 List 的子类。但是此类也是 Queue 接口的子类,Queue 接口定义了如下的方法:

3、Set接口

重点:
  1. Set 接口也是 Collection 的子接口,与 List 接口最大的不同在于,Set 接口里面的内容是不允许重复的。
  2. Set 接口并没有对 Collection 接口进行扩充,基本上还是与 Collection 接口保持一致。因为此接口没有 List 接口中定义 的 get(int index)方法,所以无法使用循环进行输出。
  3. 此接口中有两个常用的子类:HashSet、TreeSet

3.1 HashSet

ava.util.HashSet 是 Set 接口的一个实现类,它所存储的元素是不可重复的,并且元素都是无序的 (即存取顺序不一致)。 java.util.HashSet 底层的实现其实是一个 java.util.HashMap 支持,利用键值部分不可重复将其用作HashSet,value部分都对应一个默认元素,键值部分存储时无序。

HashSet 是根据对象的哈希值来确定元素在集合中的存储位置,因此具有良好的存取和查找性能。保证 元素唯一性的方式依赖于: hashCode 与 equals 方法。

当一个对象被存进 HashSet 集合后,就不能修改这个对象中的那些参与计算的哈希值的字段了,否则,对象被修改后的哈希值与最初存储进 HashSet 集合中时的哈希值就不同了,在这种情况下,即使在 contains 方法使用该对象的当前引用作为 的参数去 HashSet 集合中检索对象,也将返回找不到对象的结果,这也会导致无法从 HashSet 集合中删除当前对象,从而 造成内存泄露。

虽然HashSet无序,但是仍然可以通过遍历访问数据,借用Collection接口的方法,转换为数组即可:

Set<String> all = new HashSet<String>(); // 实例化Set接口对象
String[] str = all.toArray(new String[] {});// 变为指定的泛型类型数组
for (int x = 0; x < str.length; x++) {
    System.out.print(str[x] + "、");
}
存储流程:

给HashSet中存放自定义类型元素时,需要重写对象中的hashCode和equals方法,建立自己的比较方 式,才能保证HashSet集合中的对象唯一。如果想用HashSet存储并保证数据存储的顺序,可以使用在HashSet下面的一个子类 java.util.LinkedHashSet ,它是链表和哈希表组合的一个数据存储结构。

3.2、TreeSet

与 HashSet 不同的是,TreeSet 本身属于排序的子类。

定义:
public class TreeSet<E> extends AbstractSet<E>
implements NavigableSet<E>, Cloneable, Serializable

添加到TreeSet中的对象必须实现Comparable接口,才能实现排序。实现这个接口,需要实现里面的compareTo方法。

TreeSet有序采用二叉树进行存储,内部通过TreeMap进行实现。

什么是迭代器快速失败:

迭代器遍历集合时,遍历集合本身。如果创建迭代器后的任何时间修改集合,除了通过remove方式,迭代器将抛出ConcurrentModificationException异常。因此在并发修改的情况下,迭代器快速失败,减少未来的不确定性风险。

什么是迭代器安全失败:

遍历的是集合的备份,不会出现快速失败(一般默认)。

如果使用TreeSet添加自定义的对象,必须实现Comparable接口,提供compareTo方法。注意compareTo的实现,当两个元素相等时返回0,此时TreeSet对于后添加的元素拒绝,因为不接收两个同样的元素。

补充:Comparator接口

Comparable:强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序,类的 compareTo方法被称为它的自然比较方法。只能在类中实现compareTo()一次,不能经常修改类的代码 实现自己想要的排序。实现此接口的对象列表(和数组)可以通过Collections.sort(和Arrays.sort)进行自动排序,对象可以用作有序映射中的键或有序集合中的元素,无需指定比较器。

Comparator强行对某个对象进行整体排序。可以将Comparator 传递给sort方法(如Collections.sort 或 Arrays.sort),从而允许在排序顺序上实现精确控制。还可以使用Comparator来控制某些数据结构 (如有序set或有序映射)的顺序,或者为那些没有自然顺序的对象collection提供排序,需要实现一个compare方法。

3.3关于重复元素判断

Set 接口定义的时候本身就是不允许重复元素的,按照这个思路,如果现在真的是有重复元素的话,使用 HashSet 也同样可以进行区分。

Set<Person> all = new HashSet<Person>();
all.add(new Person("张三", 10));
all.add(new Person("李四", 10));
all.add(new Person("李四", 10));
all.add(new Person("王五", 11));
all.add(new Person("赵六", 12));
all.add(new Person("孙七", 13));
System.out.println(all);

此时发现,并没有去掉所谓的重复元素,也就是说之前的操作并不是真正的重复元素的判断,而是通过 Comparable 接口间接完成的。 如果要想判断两个对象是否相等,则必须使用 Object 类中的 equals()方法。 从最正规的来讲,如果要想判断两个对象是否相等,则有两种方法可以完成:

  1. 第一步判断两个对象的编码是否一致,这个方法需要通过 hashCode()完成,即:每个对象有唯一的编码
  2. 第二步验证对象中的每个属性是否相等,需要通过 equals()完成。

所以此时需要覆写 Object 类中的 hashCode()方法,此方法表示一个唯一的编码,一般是通过公式计算出来的。

4、Iterator

Iterator 属于迭代输出,基本的操作原理:是不断的判断是否有下一个元素,有的话,则直接输出。

定义:
public interface Iterator<E>

要想使用此接口,则必须使用 Collection 接口,在 Collection 接口中规定了一个 iterator()方法,可以用于为 Iterator 接口进行实例化操作。

方法:

通过 Collection 接口为其进行实例化之后,一定要记住,Iterator 中的操作指针是在第一条元素之上,当调用 next()方 法的时候,获取当前指针指向的值并向下移动,使用 hasNext()可以检查序列中是否还有元素。

应用:
Collection<String> all = new ArrayList<String>();
    all.add("A");
    all.add("B");
    all.add("C");
    all.add("D");
    all.add("E");
    Iterator<String> iter = all.iterator();
    while (iter.hasNext()) {// 判断是否有下一个元素
        String str = iter.next(); // 取出当前元素
        System.out.print(str + "、");
    }
}

在使用 Iterator 输出的时候有一点必须注意,在进行迭代输出的时候如果要想删除当前元素,则只能使用 Iterator 接口中的 remove()方法,而不能使用集合中的 remove()方法。否则将出现未知的错误。

Iterator 接口本身可以完成输出的功能,但是此接口只能进行由前向后的单向输出。如果要想进行双向输出,则必须 使用其子接口 —— ListIterator。

五、ListIterator

ListIterator 是可以进行双向输出的迭代接口,此接口定义如下:

public interface ListIterator<E>
extends Iterator<E>
方法:

但是如果要想使用 ListIterator 接口,则必须依靠 List 接口进行实例化。List 接口中定义了以下的方法:ListIterator listIterator()。

此处有一点需要注意的是,如果要想进行由后向前的输出,则首先必须先进行由前向后的输出。因为要将迭代器的位置后移。

六、Map接口

多值接口,里面的所有内容都按照 keyvalue 的形式保存,也称为二元偶对象。与collection根接口同一级别,多值集合的根接口,存储的是一个个键值对,通过键来访问值,key不可以重复。

定义:
public interface Map<K,V>
方法:

Map 本身是一个接口,所以一般会使用以下的几个子类:HashMap、TreeMap、Hashtable

6.1、HashMap

定义:
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
应用:
map.put(1, "张三A");
map.put(1, "张三B"); // 新的内容替换掉旧的内容
map.put(2, "李四");
map.put(3, "王五");
String val = map.get(6);

get方法根据指定的 key 找到内容,如果没有找到,则返回 null,找到 了则返回具体的内容。

Map<Integer, String> map = new HashMap<Integer, String>();
map.put(1, "张三A");
map.put(2, "李四");
map.put(3, "王五");
Set<Integer> set = map.keySet(); // 得到全部的key
Collection<String> value = map.values(); // 得到全部的value
Iterator<Integer> iter1 = set.iterator();
Iterator<String> iter2 = value.iterator();
System.out.print("全部的key:");
while (iter1.hasNext()) {
    System.out.print(iter1.next() + "、");
}
System.out.print("\n全部的value:");
while (iter2.hasNext()) {
    System.out.print(iter2.next() + "、");
}

在JDK1.8之前,哈希表底层采用数组+链表实现,即使用链表处理冲突,同一hash值的链表都存储在一 个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率 较低。而JDK1.8中,哈希表存储采用数组+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转 换为红黑树,这样大大减少了查找时间。 简单的来说,哈希表是由数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的,如下图所示。

初始桶数量是16,装载因子为0.75,超过装载因子时扩容,容量翻倍。

构造方法:
HashMap() 使用默认初始容量(16)和默认加载因子(0.75)构造一个空 HashMap
HashMap(int initialCapacity) 使用指定的初始容量和默认加载因子(0.75)构造一个空 HashMap
HashMap(int initialCapacity, float loadFactor) 使用指定的初始容量和加载因子构造一个空 HashMap
HashMap(Map<? extends K,? extends V> m) 构造一个新的 HashMap ,其映射与指定的 Map相同。
添加元素过程:

装载因子过小,查找效率高,但是存储空间利用率低。装载因子过大,查找效率慢,但是存储空间利用率高。一般要选取平衡,选取好初始容量,装载因子默认0.75。

已经存储在Map中的自定义对象如果作为键值存储不要修改它,否则由于hashcode方法和equals方法的限制,在修改键值和再散列后的场景下,都无法找到原有对象。

6.2 HashTable与HashMap的区别

6.3 关于Map集合的输出

1、 使用 Map 接口中的 entrySet()方法将 Map 接口的全部内容变为 Set 集合
2、 可以使用 Set 接口中定义的 iterator()方法为 Iterator 接口进行实例化
3、 之后使用 Iterator 接口进行迭代输出,每一次的迭代都可以取得一个 Map.Entry 的实例
4、 通过 Map.Entry 进行 key 和 value 的分离

Map.Entry 本身是一个接口。此接口是定义在 Map 接口内部的,是 Map 的内部接口。此内部接口使用 static 进行定义, 所以此接口将成为外部接口。 实际上来讲,对于每一个存放到 Map 集合中的 key 和 value 都是将其变为了 Map.Entry 并且将 Map.Entry 保存在了 Map 集合之中。

Map.Entry接口:
方法:
应用:
Set<Map.Entry<String, String>> set = map.entrySet();// 变为Set实例
Iterator<Map.Entry<String, String>> iter = set.iterator();
while (iter.hasNext()) {
    Map.Entry<String, String> me = iter.next();
    System.out.println(me.getKey() + " --> " + me.getValue());
}

Map 集合中每一个元素都是 Map.Entry 的实例,只有通过 Map.Entry 才能进行 key 和 value 的分离操作。

或使用foreach:
Map<String, String> map = new HashMap<String, String>();
map.put("ZS", "张三");
map.put("LS", "李四");
map.put("WW", "王五");
map.put("ZL", "赵六");
map.put("SQ", "孙七");
for (Map.Entry<String, String> me : map.entrySet()) {
System.out.println(me.getKey() + " --> " + me.getValue());
}

七、Collections类

Collections 实际上是一个集合的操作类,此类的定义如下:

public class Collections extends Object

这个类与 Collection 接口没有任何的关系。是一个单独存在的类。

方法:
//很多,这里仅举例
Collections.emptyList();// 空的集合

List<String> all = new ArrayList<String>();
Collections.addAll(all, "A", "B", "C");// 向集合增加元素

但是,从实际考虑,使用此类操作并不是很方便,最好的做法就是使用各个接口的直接操作的方法完成。此类只是 一个集合的操作类。

八、分析 equals、hashCode 与内存泄露

在 java 的集合中,判断两个对象是否相等的规则是:

(1)判断两个对象的 hashCode 是否相等 
        如果不相等,认为两个对象也不相等,完毕 
        如果相等,转入 2 (这一点只是为了提高存储效率而要求的,其实理论上没有也可以,但        如果没有,实际使用时效率会大大降低,所以我们 这里将其做为必需的。后面会重点讲        到这个问题。) 
        
(2)判断两个对象用 equals 运算是否相等 如果不相等,认为两个对象也不相等 如果相等,认为两个对象相等(equals()是判断两个对象是否相等的关键)

当一个对象被存进 HashSet 集合后,就不能修改这个对象中的那些参与计算的哈希值的字段了,否则,对象被修改后的哈 希值与最初存储进 HashSet 集合中时的哈希值就不同了,在这种情况下,即使在 contains 方法使用该对象的当前引用作为 的参数去 HashSet 集合中检索对象,也将返回找不到对象的结果,这也会导致无法从 HashSet 集合中删除当前对象,从而 造成内存泄露。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,406评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,732评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,711评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,380评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,432评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,301评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,145评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,008评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,443评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,649评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,795评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,501评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,119评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,731评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,865评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,899评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,724评论 2 354