泛型
泛型:定义方法时有形参,然后调用此方法时传递实参。
引入一个类型变量T(其他大写字母也可以,不过常用的就是T,E,K,V等),并且用<>括起来,并放在类名的后面。泛型类是允许有多个类型变量的。
常用的类型参数名称列表:
- E 元素,主要由java集合(Collections)框架使用
- K 键,主要用于表示映射中的键的参数类型
- V 值,主要用于表示映射中的值的参数类型
- N 数字,主要用于表示数字
- T 类型,主要用于表示第一类通用型参数
- S 类型,主要用于表示第二类通用型参数
- U 类型,主要用于表示第三类通用型参数
- V 类型,主要用于表示第四个通用类型参数
泛型类和泛型接口
可以为任何类、接口增加泛型声明
//泛型类:引入一个类型变量T(其他大写字母都可以,不过常用的就是T,E,K,V等)
public class NormalGeneric<T>{
private T data;
public NormalGeneric(){
}
public NormalGeneric(T data){
this();
this.data = data;
}
}
泛型接口与泛型类的定义基本相同
//泛型接口:引入一个类型变量T(其他大写字母都可以,不过常用的就是T,E,K,V等)
public interface Generator<T>{
public T next();
}
泛型类和接口的使用
实现泛型接口的类,有两种实现方法:
1、未传入泛型实参时:
public static void main(String[] args){
ImplGenerator<String> implGenerator = new ImplGenerator<>("XiaoMing");
System.out.println(implGenerator.next());
}
在new出类的实例时,需要指定具体类型:
public static void main(String[] args){
ImplGenerator<String> implGenerator = new ImplGenerator<>();
}
2、传入泛型实参
public class ImplGenerator2 implements Generator<Stirng>{
@Override
public String next(){
return "XiaoMing";
}
}
在new出类型的实例时,和普通的类没区别。
泛型方法
//泛型方法:引入一个类型变量T
public class GenericMethod{
//泛型方法
public <T> T genericMethos(T t){
return t;
}
//普通方法
public void test(int i,int y){
System.out.println(x+y);
}
public static void main(String[] args){
GenericMethod genericMethod = new GenericMethod();
genericMethod.test(13,7);
System.out.println(genericMethod.<~>genericMethod ("xiaoming"));
System.out.println(genericMethod.genericMethod(180));
}
}
泛型方法,是在调用方法的时候指明泛型的具体类型,泛型方法可以在任何地方和任何场景中使用,包括普通类和泛型类。
在实际开发中,经常有数值类型求和的需求,例如实现int类型的加法,有时候还需要实现long类型的求和,如果还需要double类型的求和,需要重新在重载一个输入是double类型的add方法,这时我们就可以使用泛型来处理了。
泛型的好处:
1、适合于多种数据类型执行相同的代码。
2、泛型中的类型在使用时指定,不需要强制类型转换。
泛型擦除:
java语言中的泛型,它只在程序源码中存在,在编译后的字节码文件中,就已经替换为原来的原生类型了,并且在相应的地方插入了强制转型代码。因此,对于运行期的java语言来说ArrayList与ArrayList就是同一个类,所以泛型技术实际上就是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"));
}
}
从 Signature 属性的出现可以得出结论,擦除法所谓的擦除,仅仅是对方法的 Code 属性中的字节码进行擦除,实际 上元数据中还是保留了泛型信息,这也是我们能通过反射手段取得参数化类型的根本依据。
Stream
Java8 中,Collection 新增了两个流方法,分别是 Stream() 和 parallelStream()
Java8 中添加了一个新的接口类 Stream,相当于高级版的 Iterator,它可以通过 Lambda 表达式对集合进行大批量数据操作,或 者各种非常便利、高效的聚合数据操作。
Stream 的聚合操作与数据库 SQL 的聚合操作 sorted、filter、map 等类似。我们在应用层就可以高效地实现类似数据库 SQL 的 聚合操作了,而在数据操作方面,Stream 不仅可以通过串行的方式实现数据操作,还可以通过并行的方式处理大批量数据,提高数据 的处理效率。
Stream的使用
//Stream的使用
public class StreamDemo{
public static void main(String[] agrs){
List<String> names = Arrays.asList("张三","李四","王老五","李三","张四五六");
//找出性张的最长名字的长度
int maxLenStartWithZ = names.stream().filter(name->name.startsWith("张")).mapToInt(String::length).max().getAsInt();
System.out.println(maxLenStartWithZ);
}
}
可以看到,如果实现同样的功能,那么使用循环的方式,代码行数将达到6行。
Stream操作分类
官方将stream中的操作分为两大类:终结操作和中间操作。
中间操作会返回一个新的流,一个流可以后面跟随零个或多个中间操作。其中主要是打开流,做出某种程度的数据映射/过滤,然后会返回一个新的流,交给下一个操作使用。这类操作是惰性化的,仅仅调用到这类方法,并没有真正开始流的遍历。而是在终结操作开始的时候才真正开始执行。
中间操作又可分为无状态和有状态操作,无状态是指元素处理不受之前元素的影响,有状态是指该操作只有拿到所有元素之后才能继续下去。
终结操作是指返回最终的结果。一个流只有一个终结操作,当这个操作执行后,这个流就被使用“光”了,无法再被操作。所以是这个流的最后一个操作。终结操作的执行才会真正开始流的遍历,并且会生成一个结果。
终结操作又可分为短路与非短路操作。
短路是指遇到某些符合条件的元素就可以得到最终结果。
非短路是指必须处理完所有元素才能得到最终结果。
因为stream操作类型非常多,总结一下常用的:
map():将流中的元素进行再次加工形成一个新流,流中的每一个元素映射为另一个元素。
filter():返回结果生成新的流中只包含满足筛选条件的数据。
limit():返回指定数量的元素流。返回的是stream里前面的n个元素
skip():和limit相反,将前几个元素跳过(取出)再返回一个流,如果流中的元素小于或等于n,就返回一个空的流。
sorted():将流中的元素按照自然排序方法进行排序。
distinct():将流中的元素去重之后输出。
peek():将流中每个元素执行操作,并返回一个新的流,返回的流还是包含原来流中的元素。
Stream的底层实现
一个Stream的各个操作是由处理管道组装,并统一完成数据处理的。
Stream 有中间操作和终结操作,那么对于一个写好的 Stream 处理代码来说,中间操作是通过 AbstractPipeline 生成了一个中间操作 Sink 链表 当我们调用终结操作时,会生成一个最终的 ReducingSink,通过这个 ReducingSink 触发之前的中间操作,从最后一个 ReducingSink 开始,递归产生一个 Sink 链。如下图所示:
当 Sink 链表生成完成后,Stream 开始执行,通过 spliterator 迭代集合,执行 Sink 链表中的具体操作。 java8 中的 Spliterator 的 forEachRemaining 会迭代集合,每迭代一次,都会执行一次 filter 操作,如果 filter 操作通过,就会触发 map 操作,然后将结果放入到临时数组 object 中,再进行下一次的迭代。完成中间操作后,就会触发终结操作 max。
parallelStream()
并发处理Stream,这里的并行处理指的是Stream结合了ForkJoin框架,对Stream处理进行了分片,Splititerator 中的 estimateSize 方法会估算出分 片的数据量。
通过预估的数据量获取最小处理单元的阈值,如果当前分片大小大于最小处理单元的阈值,就继续切分集合。每个分片将会生成一个 Sink 链表,当所有的分片操作完成后,ForkJoin 框架将会合并分片任何结果集。
Stream的性能:
常规数据迭代:
100 的性能对比
常规的迭代 > Stream 并行迭代> Stream 串行迭代
原因:
1、常规迭代代码简单,越简单的代码执行效率越高。 2、Stream 串行迭代,使用了复杂的设计,导致执行速度偏低。所以是性能最低的。
3、Stream 并行迭代 使用了 Fork-Join 线程池,所以效率比 Stream 串行迭代快,但是对比常规迭代还是要慢(毕竟设计和代码复杂)
大数据迭代
一亿的数组性能对比(默认线程池)
Stream 并行迭代> 常规的迭代> Stream 串行迭代
原因:
1、Stream 并行迭代 使用了 Fork-Join 线程池, 而线程池线程数为 cpu 的核心数(我的电脑为 12 核),大数据场景下,能够利用多线 程机制,所以效率比 Stream 串行迭代快,同时多线程机制切换带来的开销相对来说还不算多,所以对比常规迭代还是要快(虽然设计 和代码复杂)
2、常规迭代代码简单,越简单的代码执行效率越高。 3、Stream 串行迭代,使用了复杂的设计,导致执行速度偏低。所以是性能最低的。
一亿的数组性能对比(线程池数量=2)
原因:
Stream 并行迭代 使用了 Fork-Join 线程池,大数据场景下,虽然利用多线程机制,但是线程池线程数为 2,所以多个请求争抢着执行任 务,想象对请求来说任务是被交替执行完成,所以对比常规迭代还是要慢(虽然用到了多线程技术)
一亿的数组性能对比(线程池数量=240)
原因:
Stream 并行迭代 使用了 Fork-Join 线程池, 而线程池线程数为 240,大数据场景下,虽然利用多线程机制,但是线程太多,线程的上下 文切换成本过高,所以导致了执行效率反而没有常规迭代快。
如何合理使用stream
在循环迭代次数较少的情况下,常规的迭代方式性能反而更好,而在大数据循环迭代中,parallelStream(合理的线程池数上)有一定的优势。
但是由于所有使用并行流parallelStream的地方都是使用同一个Fork-Join线程池,而线程池线程数公为cpu的核心数。
注:如果对底层不太熟悉的放大尽量不要乱用并行流parallelStreamm(尤其是你的服务器核心数比较少的情况下)