一、泛型
泛型,即“参数化类型”。就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定 义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。
(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接口
重点:
- Set 接口也是 Collection 的子接口,与 List 接口最大的不同在于,Set 接口里面的内容是不允许重复的。
- Set 接口并没有对 Collection 接口进行扩充,基本上还是与 Collection 接口保持一致。因为此接口没有 List 接口中定义 的 get(int index)方法,所以无法使用循环进行输出。
- 此接口中有两个常用的子类: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()方法。 从最正规的来讲,如果要想判断两个对象是否相等,则有两种方法可以完成:
- 第一步判断两个对象的编码是否一致,这个方法需要通过 hashCode()完成,即:每个对象有唯一的编码
- 第二步验证对象中的每个属性是否相等,需要通过 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接口
多值接口,里面的所有内容都按照 keyvalue 的形式保存,也称为二元偶对象。与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 集合中删除当前对象,从而 造成内存泄露。
网友评论