泛型,即“参数化类型”,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。
引入一个类型变量 T(其他大写字母都可以,不过常用的就是 T,E,K,V 等等),并且用<>括起来,并放在类名的后面。泛型类是允许有多个类型变量的。
按照约定,类型参数名称命名为单个大写字母,以便可以在使用普通类或接口名称时能够容易地区分类型参数。以下是常用的类型参数名称列表:
可以为任何类、接口增加泛型声明
/**
* 泛型类
* 引入一个类型变量T(其他大写字母都可以,不过常用的就是T,E,K,V等等)
*/
public class NormalGeneric<T> {
private T data;
public NormalGeneric() {
}
public NormalGeneric(T data) {
this();
this.data = data;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public static void main(String[] args) {
NormalGeneric<String> normalGeneric = new NormalGeneric<>();
normalGeneric.setData("King");
System.out.println(normalGeneric.getData());
}
}
泛型接口与泛型类的定义基本相同。
/**
*泛型接口
* 引入一个类型变量T(其他大写字母都可以,不过常用的就是T,E,K,V等等)
*/
public interface Generator<T> {
public T next();
}
实现泛型接口的类,有两种方式:
泛型方法,是在调用方法的时候指明泛型的具体类型 ,泛型方法可以在任何地方和任何场景中使用,包括普通类和泛型类。
通过两段代码我们就可以知道为何我们需要泛型
/**
* 为什么需要泛型
*/
public class NeedGeneric {
public int addInt(int x,int y){
return x+y;
}
public float addFloat(float x,float y){
return x+y;
}
public Double addDouble(Double x,Double y){
return x+y;
}
//泛型方法
public <T extends Number> double add(T x,T y){
return x.doubleValue()+y.doubleValue();
}
public static void main(String[] args) {
//不使用泛型
NeedGeneric needGeneric = new NeedGeneric();
System.out.println(needGeneric.addInt(1,2));
System.out.println(needGeneric.addFloat(1.2f,2.4f));
//使用泛型
System.out.println(needGeneric.add(1,2));
System.out.println(needGeneric.add(1.2d,2.4d));
}
}
实际开发中,经常有数值类型求和的需求,例如实现 int 类型的加法,有时候还需要实现 long 类型的求和,如果还需要 double 类型的求和,需要重新在重载一个输入是 double 类型的 add 方法。
所以泛型的好处就是:
Java 语言中的泛型,它只在程序源码中存在,在编译后的字节码文件中,就已经替换为原来的原生类型(Raw Type,也称为裸类型)了,并且在相应的地方插入了强制转型代码,因此,对于运行期的 Java 语言来说,ArrayList<int>与 ArrayList<String>就是同一个类,所以泛型技术实际上是 Java 语言的一颗语法糖,Java 语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型称为伪泛型。
将一段 Java 代码编译成 Class 文件,然后再用字节码反编译工具进行反编译后,将会发现泛型都不见了,程序又变回了 Java 泛型出现之前的写法,泛型类型都变回了原生类型(因为被泛型擦除了)
源代码
/**
* 泛型擦除
*/
public class Theory {
public static void main(String[] args) {
Map<String,String> map = new HashMap<>();
map.put("King","18");
System.out.println(map.get("King"));
}
}
反编译后的代码
public class Theory
{
public static void main(String[] args)
{
Map<String, String> map = new HashMap();
map.put("King", "18");
System.out.println((String)map.get("King"));//这里被泛型擦除了
}
}
上面这段代码在 IDEA中是不能被编译的,因为参数 List<Integer>和 List<String>编译之后都被擦除了,变成了一样的原生类型 List<E>,擦除动作导致这两种方法的特征签名变得一模一样(注意在 IDEA 中是不行的,但是 jdk 的编译器是可以,因为 jdk 是根据方法返回值+方法名+参数)。
JVM 版本兼容性问题:JDK1.5 以前,为了确保泛型的兼容性,JVM 除了擦除,其实还是保留了泛型信息(Signature 是其中最重要的一项属性,它的作用就是存储一个方法在字节码层面的特征签名,这个属性中保存的参数类型并不是原生类型,而是包括了参数化类型的信息)----弱记忆
另外,从 Signature 属性的出现我们还可以得出结论,擦除法所谓的擦除,仅仅是对方法的 Code 属性中的字节码进行擦除,实际上元数据中还是保留了泛型信息,这也是我们能通过反射手段取得参数化类型的根本依据。
Java8 中,Collection 新增了两个流方法,分别是 Stream() 和 parallelStream()
Java8 中添加了一个新的接口类 Stream,相当于高级版的 Iterator,它可以通过 Lambda 表达式对集合进行大批量数据操作,或者各种非常便利、高效的聚合数据操作。
在 Java8 之前,我们通常是通过 for 循环或者 Iterator 迭代来重新排序合并数据,又或者通过重新定义 Collections.sorts 的Comparator 方法来实现,这两种方式对于大数据量系统来说,效率并不是很理想。
Stream 的聚合操作与数据库 SQL 的聚合操作 sorted、filter、map 等类似。我们在应用层就可以高效地实现类似数据库 SQL 的聚合操作了,而在数据操作方面,Stream 不仅可以通过串行的方式实现数据操作,还可以通过并行的方式处理大批量数据,提高数据的处理效率。
下面通过一段代码来说明使用Stream的好处:
示例代码
/**
* Stream使用入门
*/
public class StreamDemo {
public static void main(String[] args) {
List<String> names = Arrays.asList("张三", "李四", "王老五", "李三", "刘老四", "王小二", "张四", "张五六七");
//找出姓张的最长名字的长度
// List ll = new ArrayList();
// for(String name:names){
// if(name.startsWith("张")){
// ll.add(name.length());
// }
// }
// int maxLenZ = (int) Collections.max(ll);
// System.out.println(maxLenZ);
//使用stream一行代码解决了。
int maxLenZ = names.parallelStream()
.filter(name -> name.startsWith("张"))
.mapToInt(String::length)
.max()
.getAsInt();
System.out.println(maxLenZ);
}
}
可以看到,如果实现同样的功能,那么使用循环的方式,代码行数将达到 6 行。而使用Stream则只需要一行,代码简洁,可读写性好
官方将 Stream 中的操作分为两大类:终结操作(Terminal operations)和中间操作(Intermediate operations)。
中间操作会返回一个新的流,一个流后面可以跟随零个或多个中间操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后会返回一个新的流,交给下一个操作使用。这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并没有真正开始流的遍历。而是在终结操作开始的时候才真正开始执行。
中间操作又可以分为无状态(Stateless)与有状态(Stateful)操作,无状态是指元素的处理不受之前元素的影响,有状态是指该操作只有拿到所有元素之后才能继续下去。
终结操作是指返回最终的结果。一个流只能有一个终结操作,当这个操作执行后,这个流就被使用“光”了,无法再被操作。所以这必定是这个流的最后一个操作。终结操作的执行才会真正开始流的遍历,并且会生成一个结果。
终结操作又可以分为短路(Short-circuiting)与非短路(Unshort-circuiting)操作,
短路是指遇到某些符合条件的元素就可以得到最终结果,非短路是指必须处理完所有元素才能得到最终结果。操作分类详情如下图所示:
下面通过一个案例来演示Stream的使用
/**
* Stream使用
*/
public class StuWithStream {
public static void main(String[] args) {
List<Student> studentList =Datainit();
groupBy(studentList);
// filter(studentList);
total(studentList);
MaxAndMin(studentList);
}
public static List<Student> Datainit(){
List<Student> students = Arrays.asList(
new Student("小明", 168, "男"),
new Student("大明", 182, "男"),
new Student("小白", 174, "男"),
new Student("小黑", 186, "男"),
new Student("小红", 156, "女"),
new Student("小黄", 158, "女"),
new Student("小青", 165, "女"),
new Student("小紫", 172, "女"));
return students;
}
//Stream实现分组
public static void groupBy(List<Student> studentsList){
Map<String, List<Student>> groupBy = studentsList
.stream()
.collect(Collectors.groupingBy(Student::getSex));
System.out.println("分组后:"+groupBy);
}
//Stream实现过滤
public static void filter(List<Student> studentsList){
List<Student> filter = studentsList
.stream()
.filter(student->student.getHeight()>180)
.collect(Collectors.toList());
System.out.println("过滤后:"+filter);
}
//Stream实现求和
public static void total(List<Student> studentsList){
int totalHeight = studentsList
.stream()
.mapToInt(Student::getHeight)
.sum();
System.out.println(totalHeight);
}
//Stream找最大和最小
public static void MaxAndMin(List<Student> studentsList){
int maxHeight = studentsList
.stream()
.mapToInt(Student::getHeight)
.max()
.getAsInt();
System.out.println("max:"+maxHeight);
int minHeight = studentsList
.stream()
.mapToInt(Student::getHeight)
.min()
.getAsInt();
System.out.println("min:"+minHeight);
}
}
Stream 操作类型非常多,下面总结一下常用的方法:
map()
将流中的元素进行再次加工形成一个新流,流中的每一个元素映射为另外的元素。
filter()
返回结果生成新的流中只包含满足筛选条件的数据
limit()
返回指定数量的元素的流。返回的是 Stream 里前面的 n 个元素。
skip()
和 limit()相反,将前几个元素跳过(取出)再返回一个流,如果流中的元素小于或者等于 n,就会返回一个空的流。
sorted()
将流中的元素按照自然排序方式进行排序。
distinct()
将流中的元素去重之后输出。
一个 Stream 的各个操作是由处理管道组装,并统一完成数据处理的。
我们知道 Stream 有中间操作和终结操作,那么对于一个写好的 Stream 处理代码来说,中间操作是通过 AbstractPipeline 生成了一个中间操作 Sink 链表当我们调用终结操作时,会生成一个最终的 ReducingSink,通过这个 ReducingSink 触发之前的中间操作,从最后一个 ReducingSink 开始,递归产生一个 Sink链。如下图所示:
下面结合一段代码来跟踪Stream的源码实现
/**
* Stream使用入门
*/
public class StreamDemo {
public static void main(String[] args) {
List<String> names = Arrays.asList("张三", "李四", "王老五", "李三", "刘老四", "王小二", "张四", "张五六七");
//找出姓张的最长名字的长度
//使用stream一行代码解决了。
int maxLenZ = names.stream()//stream()->串行处理,parallelStream()->并行处理
.filter(name -> name.startsWith("张"))
.mapToInt(String::length)
.max()
.getAsInt();
System.out.println(maxLenZ);
}
}
串行实现
(1)跟踪stream 方法——生成一个Sink 链表的头
因为 names 是 ArrayList 集合,所以 names.stream() 方法将会调用集合类基础接口 Collection 的 Stream 方法,接着,Stream 方法就会调用 StreamSupport 类的 Stream 方法,
继续跟进 StreamSupport 类的 Stream 方法
可以看到:Stream 方法中初始化了一个 ReferencePipeline 的 Head 内部类对象(可以理解为生成一个Sink 链表的头):
(2)跟踪filter 方法
这个方法是无状态的中间操作,所以执行 filter 时,并没有进行任何的操作,而是创建了一个 Stage 来标识用户的操作。
而通常情况下 Stream 的操作又需要一个回调函数,所以一个完整的 Stage 是由数据来源、操作、回调函数组成的三元组来表示。
如下图所示,是 ReferencePipeline 的 filter 方法(Ctrl+Alt+B):
继续跟进 super方法,
(3)map 方法
与filter 方法一样都是中间操作,这里就不跟踪了
(4)跟踪max 方法
选中max方法按 Ctrl+Alt+B 快捷键
继续跟进,
Ctrl+Alt+B 继续跟进
执行完返回上层方法
当 Sink 链表生成完成后,Stream 开始执行,通过 spliterator 迭代集合,执行 Sink 链表中的具体操作。
java8 中的 Spliterator 的 forEachRemaining 会迭代集合,每迭代一次,都会执行一次 filter 操作,如果 filter 操作通过,就会触发 map操作,然后将结果放入到临时数组 object 中,再进行下一次的迭代。完成中间操作后,就会触发终结操作 max。
并行实现
并发的处理函数使用 parallelStream()方法
串行处理和并行处理在代码上的区别就在于终结操作的方法中的入口不一样,其他的中间操作方法都一样,所以这里就不再跟踪代码了
下面通过一个案例来测试Stream 的性能
示例代码
/**
* 常规的迭代
*/
public class IteratorTest {
public static void IteratorForIntTest(int[] arr) {
long timeStart = System.currentTimeMillis();
int min = Integer.MAX_VALUE;
for(int i=0; i<arr.length; i++){
if(arr[i]<min)
min = arr[i];
}
long timeEnd = System.currentTimeMillis();
System.out.println("Iterator 比较int最小值 花费的时间" + (timeEnd - timeStart));
}
/**
*Stream 串行迭代
*/
public class SerialStreamTest {
public static void SerialStreamForIntTest(int[] arr) {
long timeStart = System.currentTimeMillis();
Arrays.stream(arr).min().getAsInt();
long timeEnd = System.currentTimeMillis();
System.out.println("SerialStream串行 比较int最小值 花费的时间" + (timeEnd - timeStart));
}
public static void SerialStreamForObjectTest(List<Student> studentsList) {
long timeStart = System.currentTimeMillis();
Map<String, List<Student>> stuMap = studentsList.stream()
.filter((Student s) -> s.getHeight() > 160)
.collect(Collectors.groupingBy(Student ::getSex));
long timeEnd = System.currentTimeMillis();
System.out.println("Stream串行花费的时间" + (timeEnd - timeStart));
}
}
/**
*Stream 并行迭代
*/
public class ParallelStreamTest {
public static void ParallelStreamForIntTest(int[] arr) {
long timeStart = System.currentTimeMillis();
Arrays.stream(arr).parallel().min().getAsInt();
long timeEnd = System.currentTimeMillis();
System.out.println("ParallelStream 比较int最小值 花费的时间" + (timeEnd - timeStart));
}
public static void ParallelStreamForObjectTest(List<Student> studentsList) {
long timeStart = System.currentTimeMillis();
Map<String, List<Student>> stuMap = studentsList.parallelStream()
.filter((Student s) -> s.getHeight() > 160)
.collect(Collectors.groupingBy(Student::getSex));
long timeEnd = System.currentTimeMillis();
System.out.println("ParallelStream并行花费的时间" + (timeEnd - timeStart));
}
}
/**
* 性能测试对比
*/
public class App
{
public static void main( String[] args )
{
//100个
int[] arr = new int[100];
//parallelStream的地方都是使用同一个Fork-Join线程池,而线程池线程数仅为cpu的核心数
//可以通过一下配置修改这个值
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism","240");
Random r = new Random();
for(int i=0; i<arr.length; i++){
arr[i] = r.nextInt();
}
IteratorTest.IteratorForIntTest(arr);//1
SerialStreamTest.SerialStreamForIntTest(arr);//2
ParallelStreamTest.ParallelStreamForIntTest(arr);//3
}
}
public class App
{
public static void main( String[] args )
{
//一个亿
int[] arr = new int[100000000];
Random r = new Random();
for(int i=0; i<arr.length; i++){
arr[i] = r.nextInt();
}
IteratorTest.IteratorForIntTest(arr);//1
SerialStreamTest.SerialStreamForIntTest(arr);//2
ParallelStreamTest.ParallelStreamForIntTest(arr);//3
}
}
运行结果
Iterator 比较int最小值 花费的时间37
SerialStream串行 比较int最小值 花费的时间130
ParallelStream 比较int最小值 花费的时间28
结论
Stream 并行迭代 > 常规的迭代 > Stream 串行迭代
为什么这样:
public class App
{
public static void main( String[] args )
{
//一个亿
int[] arr = new int[100000000];
//parallelStream的地方都是使用同一个Fork-Join线程池,而线程池线程数仅为cpu的核心数
//可以通过一下配置修改这个值
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism","2");
Random r = new Random();
for(int i=0; i<arr.length; i++){
arr[i] = r.nextInt();
}
IteratorTest.IteratorForIntTest(arr);//1
SerialStreamTest.SerialStreamForIntTest(arr);//2
ParallelStreamTest.ParallelStreamForIntTest(arr);//3
}
}
运行结果
Iterator花费的时间34
Stream串行花费的时间134
ParallelStream并行花费的时间45
为什么这样:
public class App
{
public static void main( String[] args )
{
//一个亿
int[] arr = new int[100000000];
//parallelStream的地方都是使用同一个Fork-Join线程池,而线程池线程数仅为cpu的核心数
//可以通过一下配置修改这个值
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism","240");
Random r = new Random();
for(int i=0; i<arr.length; i++){
arr[i] = r.nextInt();
}
IteratorTest.IteratorForIntTest(arr);//1
SerialStreamTest.SerialStreamForIntTest(arr);//2
ParallelStreamTest.ParallelStreamForIntTest(arr);//3
}
}
运行结果
Iterator花费的时间50
Stream串行花费的时间122
ParallelStream并行花费的时间96
为什么这样:
通过上述4个案例我们可以看到:
本系列文章皆是笔者对所学的归纳总结,由于本人学识有限,如有错误之处,望各位看官指正,最后用一句话和大家共勉:当你的才华不足以撑起你的野心的时候,你该静下心来,好好学习,好好沉淀!