⑴ java集合类哪个函数可以
java集合里面的函数
java集合里面的函数_java集合【1】——— 从集合接口框架说起
百里方欣
原创
关注
0点赞·155人阅读
(一) java集合分类
之前大概分为三种,Set,List,Map三种,JDK5之后,增加Queue.主要由Collection和Map两个接口衍生出来,同时Collection接口继承Iterable接口,所以我们也可以说java里面的集合类主要是由Iterable和Map两个接口以及他们的子接口或者其实现类组成。我们可以认为Collection接口定义了单列集合的规范,每次只能存储一个元素,而Map接口定义了双列集合的规范,每次能存储一对元素。
Iterable接口:主要是实现遍历功能
Collection接口: 允许重复
Set接口:无序,元素不可重复,访问元素只能通过元素本身来访问。
List接口:有序且可重复,可以根据元素的索引来访问集合中的元素。
Queue接口:队列集合
Map接口:映射关系,简单理解为键值对,Key不可重复,与Collection接口关系不大,只是个别函数使用到。
整个接口框架关系如下(来自网络):
(1) Iterable接口
1. 内部定义的方法
java集合最源头的接口,实现这个接口的作用主要是集合对象可以通过迭代器去遍历每一个元素。
源码如下:
// 返回一个内部元素为T类型的迭代器(JDK1.5只有这个接口)
Iterator iterator();
// 遍历内部元素,action意思为动作,指可以对每个元素进行操作(JDK1.8添加)
default void forEach(Consumer super T> action) {}
// 创建并返回一个可分割迭代器(JDK1.8添加),分割的迭代器主要是提供可以并行遍历元素的迭代器,可以适应现在cpu多核的能力,加快速度。
default Spliterator spliterator() {
return Spliterators.spliteratorUnknownSize(iterator(), 0);
}
从上面可以看出,foreach迭代以及可分割迭代,都加了default关键字,这个是Java 8 新的关键字,以前接口的所有接口,具体子类都必须实现,而对于deafult关键字标识的方法,其子类可以不用实现,这也是接口规范发生变化的一点。
下面我们分别展示三个接口的调用:
1.1 iterator方法
public static void iteratorHasNext(){
List list=new ArrayList();
list.add("Jam");
list.add("Jane");
list.add("Sam");
// 返回迭代器
Iterator iterator=list.iterator();
// hashNext可以判断是否还有元素
while(iterator.hasNext()){
//next()作用是返回当前指针指向的元素,之后将指针移向下个元素
System.out.println(iterator.next());
}
}
当然也可以使用for-each loop方式遍历
for (String item : list) {
System.out.println(item);
}
但是实际上,这种写法在class文件中也是会转成迭代器形式,这只是一个语法糖。class文件如下:
public class IterableTest {
public IterableTest() { }
public static void main(String[] args) {
iteratorHasNext();
}
public static void iteratorHasNext() {
List list = new ArrayList();
list.add("Jam");
list.add("Jane");
list.add("Sam");
Iterator iterator = list.iterator();
Iterator var2 = list.iterator();
while(var2.hasNext()) {
String item = (String)var2.next();
System.out.println(item);
}
}
}
需要注意的一点是,迭代遍历的时候,如果删除或者添加元素,都会抛出修改异常,这是由于快速失败【fast-fail】机制。
public static void iteratorHasNext(){
List list=new ArrayList();
list.add("Jam");
list.add("Jane");
list.add("Sam");
for (String item : list) {
if(item.equals("Jam")){
list.remove(item);
}
System.out.println(item);
}
}
从下面的错误我们可以看出,第一个元素是有被打印出来的,也就是remove操作是成功的,只是遍历到第二个元素的时候,迭代器检查,发现被改变了,所以抛出了异常。
Jam
Exception in thread "main" java.util.
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at IterableTest.iteratorHasNext(IterableTest.java:15)
at IterableTest.main(IterableTest.java:7)
1.2 forEach方法
其实就是把对每一个元素的操作当成了一个对象传递进来,对每一个元素进行处理。
default void forEach(Consumer super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
```java
当然像ArrayList自然也是有自己的实现的,那我们就可以使用这样的写法,简洁优雅。forEach方法在java8中参数是`java.util.function.Consumer`,可以称为**消费行为**或者说**动作**类型。
```java
list.forEach(x -> System.out.print(x));
同时,我们只要实现Consumer接口,就可以自定义动作,如果不自定义,默认迭代顺序是按照元素的顺序。
public class ConsumerTest {
public static void main(String[] args) {
List list=new ArrayList();
list.add("Jam");
list.add("Jane");
list.add("Sam");
MyConsumer myConsumer = new MyConsumer();
Iterator it = list.iterator();
list.forEach(myConsumer);
}
static class MyConsumer implements Consumer {
@Override
public void accept(Object t) {
System.out.println("自定义打印:" + t);
}
}
}
输出的结果:
自定义打印:Jam
自定义打印:Jane
自定义打印:Sam
1.3 spliterator方法
这是一个为了并行遍历数据元素而设计的迭代方法,返回的是Spliterator,是专门并行遍历的迭代器。以发挥多核时代的处理器性能,java默认在集合框架中提供了一个默认的Spliterator实现,底层也就是Stream.isParallel()实现的,我们可以看一下源码:
// stream使用的就是spliterator
default Stream stream() {
return StreamSupport.stream(spliterator(), false);
}
default Spliterator spliterator() {
return Spliterators.spliterator(this, 0);
}
public static Stream stream(Spliterator spliterator, boolean parallel) {
Objects.requireNonNull(spliterator);
return new ReferencePipeline.Head<>(spliterator,
StreamOpFlag.fromCharacteristics(spliterator),
parallel);
}
使用的方法如下:
public static void spliterator(){
List list = Arrays.asList("1", "2", "3","4","5","6","7","8","9","10");
// 获取可迭代器
Spliterator spliterator = list.spliterator();
// 一个一个遍历
System.out.println("tryAdvance: ");
spliterator.tryAdvance(item->System.out.print(item+" "));
spliterator.tryAdvance(item->System.out.print(item+" "));
System.out.println("\n-------------------------------------------");
// 依次遍历剩下的
System.out.println("forEachRemaining: ");
spliterator.forEachRemaining(item->System.out.print(item+" "));
System.out.println("\n------------------------------------------");
// spliterator1:0~10
Spliterator spliterator1 = list.spliterator();
// spliterator1:6~10 spliterator2:0~5
Spliterator spliterator2 = spliterator1.trySplit();
// spliterator1:8~10 spliterator3:6~7
Spliterator spliterator3 = spliterator1.trySplit();
System.out.println("spliterator1: ");
spliterator1.forEachRemaining(item->System.out.print(item+" "));
System.out.println("\n------------------------------------------");
System.out.println("spliterator2: ");
spliterator2.forEachRemaining(item->System.out.print(item+" "));
System.out.println("\n------------------------------------------");
System.out.println("spliterator3: ");
spliterator3.forEachRemaining(item->System.out.print(item+" "));
}
tryAdvance() 一个一个元素进行遍历
forEachRemaining() 顺序地分块遍历
trySplit()进行分区形成另外的 Spliterator,使用在并行操作中,分出来的是前面一半,就是不断把前面一部分分出来
结果如下:
tryAdvance:
1 2
-------------------------------------------
forEachRemaining:
3 4 5 6 7 8 9 10
------------------------------------------
spliterator1:
8 9 10
------------------------------------------
spliterator2:
1 2 3 4 5
------------------------------------------
spliterator3:
6 7
还有一些其他的用法在这里就不列举了,主要是trySplit()之后,可以用于多线程遍历。理想的时候,可以平均分成两半,有利于并行计算,但是不是一定平分的。
2. Collection接口 extend Iterable
Collection接口可以算是集合类的一个根接口之一,一般不能够直接使用,只是定义了一个规范,定义了添加,删除等管理数据的方法。继承Collection接口的有List,Set,Queue,不过Queue定义了自己的一些接口,相对来说和其他的差异比较大。
2.1 内部定义的方法
源码如下:
boolean add(Object o) //添加元素
boolean remove(Object o) //移除元素
boolean addAll(Collection c) //批量添加
boolean removeAll(Collection c) //批量移除
void retainAll(Collection c) // 移除在c中不存在的元素
void clear() //清空集合
int size() //集合大小
boolean isEmpty() //是否为空
boolean contains(Object o) //是否包含在集合中
boolean containsAll(Collection c) //是否包含所有的元素
Iterator iterator() // 获取迭代器
Object[] toArray() // 转成数组
default boolean removeIf(Predicate super E> filter) {} // 删除集合中复合条件的元素,删除成功返回true
boolean equals(Object o)
int hashCode()
default Spliterator spliterator() {} //获取可分割迭代器
default Stream stream() {} //获取流
default Stream parallelStream() {} //获取并行流
里面获取并行流的方法parallelStream(),其实就是通过默认的ForkJoinPool(主要用来使用分治法(Divide-and-Conquer Algorithm)来解决问题),提高多线程任务的速度。我们可以使用ArrayList来演示一下平行处理能力。例如下面的例子,输出的顺序就不一定是1,2,3...,可能是乱序的,这是因为任务会被分成多个小任务,任务执行是没有特定的顺序的。
List list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
list.parallelStream()
.forEach(out::println);
2.2 继承Collection的主要接口
graph LR;
Collection -->List-有顺序,可重复
List-有顺序,可重复 -->LinkedList-使用链表实现,线程不安全
List-有顺序,可重复 -->ArrayList-数组实现,线程不安全
List-有顺序,可重复 -->Vector-数组实现,线程安全
Vector-数组实现,线程安全 -->Stack-堆栈,先进后出
Collection-->Set-不可重复,内部排序
Set-不可重复,内部排序-->HashSet-hash表存储
HashSet-hash表存储-->LinkHashSet-链表维护插入顺序
Set-不可重复,内部排序-->TreeSet-二叉树实现,排序
Collection-->Queue-队列,先进先出
2.2.1 List extend Collection
继承于Collection接口,有顺序,取出的顺序与存入的顺序一致,有索引,可以根据索引获取数据,允许存储重复的元素,可以放入为null的元素。
最常见的三个实现类就是ArrayList,Vector,LinkedList,ArrayList和Vector都是内部封装了对数组的操作,唯一不同的是,Vector是线程安全的,而ArrayList不是,理论上ArrayList操作的效率会比Vector好一些。
里面是接口定义的方法:
int size(); //获取大小
boolean isEmpty(); //判断是否为空
boolean contains(Object o); //是否包含某个元素
Iterator iterator(); //获取迭代器
Object[] toArray(); // 转化成为数组(对象)
T[] toArray(T[] a); // 转化为数组(特定位某个类)
boolean add(E e); //添加
boolean remove(Object o); //移除元素
boolean containsAll(Collection> c); // 是否包含所有的元素
boolean addAll(Collection extends E> c); //批量添加
boolean addAll(int index, Collection extends E> c); //批量添加,指定开始的索引
boolean removeAll(Collection> c); //批量移除
boolean retainAll(Collection> c); //将c中不包含的元素移除
default void replaceAll(UnaryOperator operator) {}//替换
default void sort(Comparator super E> c) {}// 排序
void clear();//清除所有的元素
boolean equals(Object o);//是否相等
int hashCode(); //计算获取hash值
E get(int index); //通过索引获取元素
E set(int index, E element);//修改元素
void add(int index, E element);//在指定位置插入元素
E remove(int index);//根据索引移除某个元素
int indexOf(Object o); //根据对象获取索引
int lastIndexOf(Object o); //获取对象元素的最后一个元素
ListIterator listIterator(); // 获取List迭代器
ListIterator listIterator(int index); // 根据索引获取当前的位置的迭代器
List subList(int fromIndex, int toIndex); //截取某一段数据
default Spliterator spliterator(){} //获取可切分迭代器
上面的方法都比较简单,值得一提的是里面出现了ListIterator,这是一个功能更加强大的迭代器,继承于Iterator,只能用于List类型的访问,拓展功能例如:通过调用listIterator()方法获得一个指向List开头的ListIterator,也可以调用listIterator(n)获取一个指定索引为n的元素的ListIterator,这是一个可以双向移动的迭代器。
操作数组索引的时候需要注意,由于List的实现类底层很多都是数组,所以索引越界会报错IndexOutOfBoundsException。
说起List的实现子类:
ArrayList:底层存储结构是数组结构,增加删除比较慢,查找比较快,是最常用的List集合。线程不安全。
LinkedList:底层是链表结构,增加删除比较快,但是查找比较慢。线程不安全。
Vector:和ArrayList差不多,但是是线程安全的,即同步。
2.2.2 Set extend Collection
Set接口,不允许放入重复的元素,也就是如果相同,则只存储其中一个。
下面是源码方法:
int size(); //获取大小
boolean isEmpty(); //是否为空
boolean contains(Object o); //是否包含某个元素
Iterator iterator(); //获取迭代器
Object[] toArray(); //转化成为数组
T[] toArray(T[] a); //转化为特定类的数组
boolean add(E e); //添加元素
boolean remove(Object o); //移除元素
boolean containsAll(Collection> c); //是否包含所有的元素
boolean addAll(Collection extends E> c); //批量添加
boolean retainAll(Collection> c); //移除所有不存在于c集合中的元素
boolean removeAll(Collection> c); //移除所有在c集合中存在的元素
void clear(); //清空集合
boolean equals(Object o); //是否相等
int hashCode(); //计算hashcode
default Spliterator spliterator() {} //获取可分割迭代器
主要的子类:
HashSet
允许空值
通过HashCode方法计算获取hash值,确定存储位置,无序。
LinkedHashSet
HashSet的子类
有顺序
TreeSet
如果无参数构建Set,则需要实现Comparable方法。
亦可以创建时传入比较方法,用于排序。
2.2.3 Queue extend Collection
队列接口,在Collection接口的接触上添加了增删改查接口定义,一般默认是先进先出,即FIFO,除了优先队列和栈,优先队列是自己定义了排序的优先顺序,队列中不允许放入null元素。
下面是源码:
boolean add(E e); //插入一个元素到队列,失败时返回IllegalStateException (如果队列容量不够)
boolean offer(E e); //插入一个元素到队列,失败时返回false
E remove(); //移除队列头的元素并移除
E poll(); //返回并移除队列的头部元素,队列为空时返回null
E element(); //返回队列头元素
E peek(); //返回队列头部的元素,队列为空时返回null
主要的子接口以及实现类有:
Deque(接口):Queue的子接口,双向队列,可以从两边存取
ArrayDeque:Deque的实现类,底层用数组实现,数据存贮在数组中
AbstractQueue:Queue的子接口,仅实现了add、remove和element三个方法
PriorityQueue:按照默认或者自己定义的顺序来排序元素,底层使用堆(完全二叉树)实现,使用动态数组实现,
BlockingQueue: 在java.util.concurrent包中,阻塞队列,满足当前无法处理的操作。
(2) Map接口
定义双列集合的规范Map,每次存储一对元素,即key和value。
key的类型可以和value的类型相同,也可以不同,任意的引用类型都可以。
key是不允许重复的,但是value是可以重复的,所谓重复是指计算的hash值系统。
下面的源码的方法:
V put(K key, V value); // 添加元素
V remove(Object key); // 删除元素
void putAll(Map extends K, ? extends V> m); // 批量添加
void clear() // 移除所有元素
V get(Object key); // 通过key查询元素
int size(); // 查询集合大小
boolean isEmpty(); // 集合是否为空
boolean containsKey(Object key); // 是否包含某个key
boolean containsValue(Object value); // 是否包含某个value
Set keySet(); // 获取所有key的set集合
Collection values(); // 获取所有的value的set集合
Set> entrySet(); // 返回键值对的set,每一个键值对是一个entry对象
boolean equals(Object o); // 用于比较的函数
int hashCode(); // 计算hashcode
default V getOrDefault(Object key, V defaultValue) // 获取key对应的Value,没有则返回默认值()
default void forEach(BiConsumer super K, ? super V> action) {} // 遍历
default void replaceAll(BiFunction super K, ? super V, ? extends V> function) {} // 批量替换
// 缺少这个key的时候才会添加进去
// 返回值是是key对应的value值,如果不存在,则返回的是刚刚放进去的value
default V putIfAbsent(K key, V value) {}
default boolean remove(Object key, Object value) {} // 移除元素
default boolean replace(K key, V oldValue, V newValue) {} // 替换
default V replace(K key, V value) {} //替换
// 和putIfAbsent有点像,只不过传进去的mappingFunction是映射函数,也就是如果不存在key对应的value,将会执行函数,函数返回值会被当成value添加进去,同时返回新的value值
default V computeIfAbsent(K key,Function super K, ? extends V> mappingFunction) {}
// 和computeIfAbsent方法相反,只有key存在的时候,才会执行函数,并且返回
default V computeIfPresent(K key,BiFunction super K, ? super V, ? extends V> remappingFunction) {}
// 不管如何都会执行映射函数,返回value
default V compute(K key,BiFunction super K, ? super V, ? extends V> remappingFunction) {}
default V merge(K key, V value,BiFunction super V, ? super V, ? extends V> remappingFunction) {}
值得注意的是,Map里面定义了一个Entry类,其实就是定义了一个存储数据的类型,一个entry就是一个.
Map的常用的实现子类:
HashMap:由数组和链表组成,线程不安全,无序。
LinkedHashMap:如果我们需要是有序的,那么就需要它,时间和空间效率没有HashMap那么高,底层是维护一条双向链表,保证了插入的顺序。
ConcurrentHashMap:线程安全,1.7JDK使用锁分离,每一段Segment都有自己的独立锁,相对来说效率也比较高。JDK1.8抛弃了Segment,使用Node数组+链表和红黑树实现,在线程安全控制上使用Synchronize和CAS,可以认为是优化的线程安全的HashMap。
HashTable:对比与HashMap主要是使用关键字synchronize,加上同步锁,线程安全。
(二)总结
这些集合原始接口到底是什么?为什么需要?
我想,这些接口其实都是一种规则/规范的定义,如果不这么做也可以,所有的子类自己实现,但是从迭代以及维护的角度来说,这就是一种抽象或者分类,比如定义了Iterator接口,某一些类就可以去继承或者实现,那就得遵守这个规范/契约。可以有所拓展,每个子类的拓展不一样,所以每个类就各有所长,但是都有一个中心,就是原始的集合接口。比如实现Map接口的所有类的中心思想都不变,只是各有所长,各分千秋,形成了大千集合世界。
【作者简介】:
秦怀,公众号【秦怀杂货店】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。个人写作方向:Java源码解析,JDBC,Mybatis,Spring,redis,分布式,剑指Offer,LeetCode等,认真写好每一篇文章,不喜欢标题党,不喜欢花里胡哨,大多写系列文章,不能保证我写的都完全正确,但是我保证所写的均经过实践或者查找资料。遗漏或者错误之处,还望指正。
平日时间宝贵,只能使用晚上以及周末时间学习写作,关注我,我们一起成长吧~
⑵ java程序是由什么组成的
要编写Java程序,首先应该知道Java程序文件中必须包括什么内容,Java程序的源程序文件结构如下:
package语句,0~1句,必须放在文件开始,作用是把当前文件放入所指向的包中。import语句,0~多句,必须放在所有类定义之前,用来引入标准类或已有类。publicclassDefinition,0~1句,文件名必须与类的类名完全相同。classDefinition,0~多句,类定义的个数不受限制。
interfaceDefinition,0~多句,接口定义的个数不受限制。Java程序的源代码文件要求包含三个要素:
1)以package开始的包声明语句,此句为可选。若有,且只能有一个package语句且只能是源程序文件的第一个语句,若没有,此文件将放到默认的当前目录下。
2)以import开始的类引入声明语句,数量可以是任意个。
3)classDefinition和interfaceDefinition分别代表类和接口的定义。由public开始的类定义只能有一个,且要求源程序文件名必须和public类名相同,Java语言对字符的大小写敏感,因此文件名相同意味着字母大小写也完全相同。如果源程序文件中有主方法main(),它应放在public类中。这三个要素在程序中必须严格按上述顺序出现。
2.类的构成
Java程序都是由类(class)所组成的,类的概念的产生是为了让程序语言能更清楚地表达出现实事物的本性。在Java中,类就是用于创建对象的模板,包含了特定对象集合的所有特性。Java类由两种不同的信息构成:属性和行为。
属性由一系列区别对象的数据组成,可用于确定属于类的对象的外观、状态和其他性质。在Java程序中,属性往往以类的成员变量形式出现。
行为指类对象对本身和其他对象所可以完成的事情,可以用于修改对象的属性,接收来自其他对象的信息和向其他要求执行任务的对象发送信息。在Java中,行为往往以一段小程序的形式出现,Java称这种小程序为“方法”(method)。统称为类的成员方法。
Java中类定义的语法形式为:
修饰符class<类名>[extend<父类名>]{type类变量1;type类变量2;
成员变量修饰符type<类方法名1>(参数列表){type局
部变量;方法体
}修饰符type<类方法名2>(参数列表){type局部变量;方法体
}成员方法}
其中class是Java的关键字,表明其后定义的是一个类。class前面的修饰符用来限定所定义的类的使用方式。类名是用户为该类起的名字,应该是一个合法的标识符。紧接着类定义语句的大括号之间的内容称为类主体。type指的是变量或方法的数据类型。类主体由成员变量和成员方法两部分组成。
以上所有提到或未提到的概念,在后面的章节中将详细为大家介绍。
3.注释语句的添加
在开发Java程序的过程中,经常需要在适当的地方加上注释语句,以便其他人阅读
程序,一般来说,Java语言的源文件中,任何地方都可以加注释语句,一个好的程序应该在
其需要的地方适当地加上一些注释,以便于其他人阅读并理解程序。
注释语句有三种格式:
1)//注释内容用于注释一行语句。
2)/ˇ注释内容ˇ/用于注释一行或多行语句。
3)/ˇˇ注释内容ˇˇ/用于注释一行或多行语句且注释语句中的内容可以通过使用Javadoc生成API文档,实现文档与程序同步实现的功能。
⑶ JAVA源代码的扩展名为( )
JAVA源代码的扩展名为.java。
Java源程序(.java文件)-java字节码文件(.class文件)-由解释执行器(java.exe)将字节码文件加载到java虚拟机(jvm)-字节码文件(.class)就会在java虚拟机中执行。
Java的基本包
java.lang其中包含有:接口:Comparable、Cloneable、Runable等;类:八个基本数据类型封装类、Byte、Short、Integer、Long、Boolean、Character、Float、Double等。
(3)java集合源码扩展阅读
Java为一个面向对象的语言。对程序员来说,这意味着要注意应中的数据和操纵数据的方法(method),而不是严格地用过程来思考。数据和方法一起描述对象(object)的状态和行为。每一对象是其状态和行为的封装。
类按一定体系和层次安排的,使得子类可以从超类继承行为。在这个类层次体系中有一个根类,它是具有一般行为的类。Java程序是用类来组织的。
Java还包括一个类的扩展集合,分别组成各种程序包(Package),用户可以在自己的程序中使用。
例如,Java提供产生图形用户接口部件的类(java.awt包),这里awt为抽象窗口工具集(abstract windowing toolkit)的缩写,处理输入输出的类(java.io包)和支持网络功能的类(java.net包)。
⑷ java iterator循环遍历集合(比如HashSet)的原理
Iterator<String> it = set.iterator() 这时得到一个迭代器,它的指针位于第一个元素之前。
然后我们不停地 hasNext() 再 next 得到值,专当hasNext 为 false 时当然就不属能去 next 了。
这背后的位置索引是在迭代器内部记录着的,我们不能直接改它的值,只能 next 去下一个。
不过像 ListIterator 则是双向的,可以 previous。递增递减都是由 next / previous 方法来操作的。
⑸ Java泛型集合的应用和方法
泛型(Generic type 或者 generics)是对 Java 语言的类型系统的一种扩展,以支持创建可以按类型进行参数化的类。可以把类型参数看作是使用参数化类型时指定的类型的一个占位符,就像方法的形式参数是运行时传递的值的占位符一样。
可以在集合框架(Collection framework)中看到泛型的动机。例如,Map 类允许您向一个 Map 添加任意类的对象,即使最常见的情况是在给定映射(map)中保存某个特定类型(比如 String)的对象。
因为 Map.get() 被定义为返回 Object,所以一般必须将 Map.get() 的结果强制类型转换为期望的类型,如下面的代码所示:
Map m = new HashMap();
m.put("key", "blarg");
String s = (String) m.get("key");
要让程序通过编译,必须将 get() 的结果强制类型转换为 String,并且希望结果真的是一个 String。但是有可能某人已经在该映射中保存了不是 String 的东西,这样的话,上面的代码将会抛出 ClassCastException。
理想情况下,您可能会得出这样一个观点,即 m 是一个 Map,它将 String 键映射到 String 值。这可以让您消除代码中的强制类型转换,同时获得一个附加的类型检查层,该检查层可以防止有人将错误类型的键或值保存在集合中。这就是泛型所做的工作。
泛型的好处
Java 语言中引入泛型是一个较大的功能增强。不仅语言、类型系统和编译器有了较大的变化,以支持泛型,而且类库也进行了大翻修,所以许多重要的类,比如集合框架,都已经成为泛型化的了。这带来了很多好处:
类型安全。 泛型的主要目标是提高 Java 程序的类型安全。通过知道使用泛型定义的变量的类型限制,编译器可以在一个高得多的程度上验证类型假设。没有泛型,这些假设就只存在于程序员的头脑中(或者如果幸运的话,还存在于代码注释中)。
Java 程序中的一种流行技术是定义这样的集合,即它的元素或键是公共类型的,比如“String 列表”或者“String 到 String 的映射”。通过在变量声明中捕获这一附加的类型信息,泛型允许编译器实施这些附加的类型约束。类型错误现在就可以在编译时被捕获了,而不是在运行时当作 ClassCastException 展示出来。将类型检查从运行时挪到编译时有助于您更容易找到错误,并可提高程序的可靠性。
消除强制类型转换。 泛型的一个附带好处是,消除源代码中的许多强制类型转换。这使得代码更加可读,并且减少了出错机会。
尽管减少强制类型转换可以降低使用泛型类的代码的罗嗦程度,但是声明泛型变量会带来相应的罗嗦。比较下面两个代码例子。
该代码不使用泛型:
List li = new ArrayList();
li.put(new Integer(3));
Integer i = (Integer) li.get(0);
该代码使用泛型:
List<Integer> li = new ArrayList<Integer>();
li.put(new Integer(3));
Integer i = li.get(0);
在简单的程序中使用一次泛型变量不会降低罗嗦程度。但是对于多次使用泛型变量的大型程序来说,则可以累积起来降低罗嗦程度。
潜在的性能收益。 泛型为较大的优化带来可能。在泛型的初始实现中,编译器将强制类型转换(没有泛型的话,程序员会指定这些强制类型转换)插入生成的字节码中。但是更多类型信息可用于编译器这一事实,为未来版本的 JVM 的优化带来可能。
由于泛型的实现方式,支持泛型(几乎)不需要 JVM 或类文件更改。所有工作都在编译器中完成,编译器生成类似于没有泛型(和强制类型转换)时所写的代码,只是更能确保类型安全而已。
泛型用法的例子
泛型的许多最佳例子都来自集合框架,因为泛型让您在保存在集合中的元素上指定类型约束。考虑这个使用 Map 类的例子,其中涉及一定程度的优化,即 Map.get() 返回的结果将确实是一个 String:
Map m = new HashMap();
m.put("key", "blarg");
String s = (String) m.get("key");
如果有人已经在映射中放置了不是 String 的其他东西,上面的代码将会抛出 ClassCastException。泛型允许您表达这样的类型约束,即 m 是一个将 String 键映射到 String 值的 Map。这可以消除代码中的强制类型转换,同时获得一个附加的类型检查层,这个检查层可以防止有人将错误类型的键或值保存在集合中。
下面的代码示例展示了 JDK 5.0 中集合框架中的 Map 接口的定义的一部分:
public interface Map<K, V> {
public void put(K key, V value);
public V get(K key);
}
注意该接口的两个附加物:
类型参数 K 和 V 在类级别的规格说明,表示在声明一个 Map 类型的变量时指定的类型的占位符。
在 get()、put() 和其他方法的方法签名中使用的 K 和 V。
为了赢得使用泛型的好处,必须在定义或实例化 Map 类型的变量时为 K 和 V 提供具体的值。以一种相对直观的方式做这件事:
Map<String, String> m = new HashMap<String, String>();
m.put("key", "blarg");
String s = m.get("key");
当使用 Map 的泛型化版本时,您不再需要将 Map.get() 的结果强制类型转换为 String,因为编译器知道 get() 将返回一个 String。
在使用泛型的版本中并没有减少键盘录入;实际上,比使用强制类型转换的版本需要做更多键入。使用泛型只是带来了附加的类型安全。因为编译器知道关于您将放进 Map 中的键和值的类型的更多信息,所以类型检查从执行时挪到了编译时,这会提高可靠性并加快开发速度。
向后兼容
在 Java 语言中引入泛型的一个重要目标就是维护向后兼容。尽管 JDK 5.0 的标准类库中的许多类,比如集合框架,都已经泛型化了,但是使用集合类(比如 HashMap 和 ArrayList)的现有代码将继续不加修改地在 JDK 5.0 中工作。当然,没有利用泛型的现有代码将不会赢得泛型的类型安全好处。
二 泛型基础
类型参数
在定义泛型类或声明泛型类的变量时,使用尖括号来指定形式类型参数。形式类型参数与实际类型参数之间的关系类似于形式方法参数与实际方法参数之间的关系,只是类型参数表示类型,而不是表示值。
泛型类中的类型参数几乎可以用于任何可以使用类名的地方。例如,下面是 java.util.Map 接口的定义的摘录:
public interface Map<K, V> {
public void put(K key, V value);
public V get(K key);
}
Map 接口是由两个类型参数化的,这两个类型是键类型 K 和值类型 V。(不使用泛型)将会接受或返回 Object 的方法现在在它们的方法签名中使用 K 或 V,指示附加的类型约束位于 Map 的规格说明之下。
当声明或者实例化一个泛型的对象时,必须指定类型参数的值:
Map<String, String> map = new HashMap<String, String>();
注意,在本例中,必须指定两次类型参数。一次是在声明变量 map 的类型时,另一次是在选择 HashMap 类的参数化以便可以实例化正确类型的一个实例时。
编译器在遇到一个 Map<String, String> 类型的变量时,知道 K 和 V 现在被绑定为 String,因此它知道在这样的变量上调用 Map.get() 将会得到 String 类型。
除了异常类型、枚举或匿名内部类以外,任何类都可以具有类型参数。
命名类型参数
推荐的命名约定是使用大写的单个字母名称作为类型参数。这与 C++ 约定有所不同(参阅 附录 A:与 C++ 模板的比较),并反映了大多数泛型类将具有少量类型参数的假定。对于常见的泛型模式,推荐的名称是:
K —— 键,比如映射的键。
V —— 值,比如 List 和 Set 的内容,或者 Map 中的值。
E —— 异常类。
T —— 泛型。
泛型不是协变的
关于泛型的混淆,一个常见的来源就是假设它们像数组一样是协变的。其实它们不是协变的。List<Object> 不是 List<String> 的父类型。
如果 A 扩展 B,那么 A 的数组也是 B 的数组,并且完全可以在需要 B[] 的地方使用 A[]:
Integer[] intArray = new Integer[10];
Number[] numberArray = intArray;
上面的代码是有效的,因为一个 Integer 是 一个 Number,因而一个 In。