从零开始学java--泛型
泛型
目录
泛型
引入
泛型类
泛型与多态
泛型方法
泛型的界限
类型擦除
函数式接口
Supplier供给型函数式接口:
Consumer消费型函数式接口:
Function函数型函数式接口:
Predicate断言式函数式接口:
判空包装
引入
学生成绩可能是数字类型,也可能是字符串类型,如何存放可能出现的两种类型呢:
public class Score {
String name;
String id;
Object value; //因为Object是所有类型的父类,因此既可以存放Integer也能存放String
public Score(String name,String id,Object value){
this.name=name;
this.id=id;
this.value=value;
}
}
以上方法虽然解决了多种类型存储的问题,但是Object类型在编译阶段并不具有良好的类型判断能力,很容易出现:
public static void main(String[] args) {
Score score=new Score("数学","aa","优秀"); //是String类型的
Integer number=(Integer) score.getValue();
//获取成绩需要进行强制类型转换,虽然并不是一开始的类型,但是编译不会报错
}
由于是Object类型,所以并不能直接判断存储的到底是String还是Integer,取值只能进行强制类型转换,显然无法在编译期确定类型是否安全,项目中代码量非常大,进行类型比较又会导致额外的开销和增加代码量,如果不比较又容易出现类型转换异常,代码的健壮性有所欠缺。
为了解决以上问题,JDK5新增了泛型,它能够在编译阶段检查类型安全,大大提升开发效率。
泛型类
定义泛型类:
public class Score<T> { //泛型类需要使用<>,在里面添加1-N个类型变量
String name;
String id;
T value; //T会根据使用时提供的类型自动变成对应类型
public Score(String name,String id,T value){ //这里的T可以是任何类型,但是一旦确定就不能修改了
this.name=name;
this.id=id;
this.value=value;
}
}
public static void main(String[] args) {
Score<String> score=new Score<>("数学","aa","优秀");
//使用时跟上<>并在其中填写明确要使用的类型
}
泛型将数据类型控制在了编译阶段, 在编写代码时就能明确泛型的类型,类型不符合将无法编译通过。
1、因为是具体使用对象时才会明确具体类型,所以说静态方法中不能用。
2、方法中使用待确定类型的变量时,因为不明确类型则会默认这个变量是一个Object类型的变量(即不能使用String等类型中的方法)。可对其进行强制类型转换但没必要。
3、不能通过这个不确定的类型变量直接创建对象和对应的数组。
4、具体类型不同的泛型类变量,不能使用不同的变量进行接收。
5、如果要让某个变量支持引用确定了任意类型的泛型,可以使用?通配符。
public static void main(String[] args) {
Score<String> score=new Score<>("数学","aa","优秀");
Score<?>score1=score;
}
如果使用通配符,由于类型不确定,所以说具体类型同样会变成Object。
6、泛型变量可以定义多个,多个类型变量用,隔开。在使用时需要将这三种类型都进行明确指令。
7、泛型只能确定为一个引用类型,不支持基本类型。
要存放基本数据类型的值,我们只能使用对应的包装类。
如果是基本类型的数组,因为数组本身是引用类型,所以是可以的。
泛型与多态
不只是类,包括接口、抽象类都可以支持泛型:
public static void main(String[] args) {
Score<String> score=new Score<>("数学","aa","优秀");
Score<?>score1=score;
}
当子类实现此接口时,我们可以选择在实现类明确泛型类型:
public static void main(String[] args) {
A a=new A();
Integer i=a.study();
}
static class A implements Study<Integer>{
//在实现接口或是继承父类时,如果子类是一个普通类,那么可以直接明确对应类型
@Override
public Integer study() {
return 0;
}
}
也可以继续使用泛型:
public static void main(String[] args) {
A<Integer> a=new A<>();
Integer i=a.study();
}
static class A<T> implements Study<T> {
//让子类继续为一个泛型类,那么可以不明确
@Override
public T study() {
return null;
}
}
继承:
static class A<T>{
}
static class B extends A<String>{
}
泛型方法
泛型变量不仅仅在泛型类中使用,也可以定义泛型方法。
当某个方法(无论是静态方法还是成员方法)需要接受的参数类型不确定时,我们可以使用泛型来表示:
public static void main(String[] args) {
String str=test("10");
}
public static <T>T test(T t){ //在返回值类型前添加<>并填写泛型变量表示这是一个泛型方法
return t;
}
泛型方法会在使用时自动确定泛型类型,比如我们定义的是类型T作为参数,同样的类型T作为返回值,实际传入的参数是一个字符串类型的值,那么T就会自动变成String类型,因此返回值也是String类型。
泛型方法在很多工具类中也有,比如说Arrays的排序方法:
public static void main(String[] args) {
Integer[] arr = {1, 3, 2, 7, 4, 9, 0};
//不能比较基本数据类型int
Arrays.sort(arr, new Comparator<Integer>() {
//通过创建泛型接口的匿名内部类,来自定义排序规则,因为匿名内部类就是接口的实现类,所以这里就明确了类型
@Override
public int compare(Integer o1, Integer o2) { //这个方法会在执行排序时被调用(别人调用我们的实现)
//想要让数据从大到小排列:
return o2-o1;
//compare方法要求返回一个int来表示两个数的大小关系,大于0表示大于,小于0表示小于
//如果o2比o1大,那么应该排在前面,所以说返回正数表示大于
}
});
System.out.println(Arrays.toString(arr));
}
可替换为Lambda表达式:
public static void main(String[] args) {
Integer[] arr = {1, 3, 2, 7, 4, 9, 0};
Arrays.sort(arr, (o1, o2) -> o2-o1);
System.out.println(Arrays.toString(arr));
}
泛型的界限
若现在没有String类型的成绩了,但是成绩依然可能是整数或小数,我们不希望将泛型指定为除数字类型外的其他类型,就需要使用到泛型的上界定义。
只需要在泛型变量的后面添加extends关键字即可指定上界:
public class Score<T extends Number> { //设定类型参数上界,必须是Number或Number的子类
String name;
String id;
T value;
public Score(String name,String id,T value){
this.name=name;
this.id=id;
this.value=value;
}
public T getValue() {
return value;
}
}
泛型通配符也支持泛型的界限:
public static void main(String[] args) {
Score<? extends Integer>score=new Score<>("xm","11",10);
}
下界只适用于通配符,对于类型变量来说是不支持的。
public static void main(String[] args) {
Score<? super Object>score=new Score<>("xm","11",10);
}
entends定义的只能存放它自己及其子类,super定义的只能存放它自己及其父类。
限定上界后使用这个对象的泛型成员:
public static void main(String[] args) {
Score<? extends Number>score=new Score<>("xm","11",10);
Number o=score.getValue(); //此时虽然使用的是通配符,但是不再是Object类型,而是对应的上界
}
限定下界的话,因为还有可能是Object,所以说依然和之前一样:
public static void main(String[] args) {
Score<? super Number>score=new Score<>("xm","11",10);
Object o=score.getValue();
}
类型擦除
实际上在Java中并不是真的有泛型类型,因为所有的对象都是一个普通的类型,一个泛型类型编译之后,实际上会直接使用默认的类型。
在编译的过程当中,将所有的T替换为Object这种机制,我们称为:擦除机制。
如果我们给类型变量设定了上界,那么会从默认类型变成上界定义的类型。
泛型其实仅仅是在编译阶段进行类型检查,当程序在运行时,并不会真的去检查对应类型,所以哪怕我们不指定类型也可以使用。
擦除机制其实就是为了方便使用后面集合类(否则每次都要强制类型转换)同时为了向下兼容采取的方案,因此泛型的使用会有一些限制:
首先,在进行类型判断时,不允许使用泛型,只能使用原始类型:
public static void main(String[] args) {
Test<String> test =new Test<>();
System.out.println(test instanceof Test);
}
其次,泛型不支持创建参数化类型数组的:
只不过只是把它当做泛型类型的数组还是可以用的:
函数式接口
@FunctionalInterface 函数式接口都会打上这样的注解
满足Lambda表达式的需求有且仅有一个需要去实现(未实现)的方法。
函数式接口就是JDK1.8专门提供好的用于Lambda表达式的接口,这些接口都可以直接使用Lambda表达式。以下主要介绍四个主要的函数式接口:
Supplier供给型函数式接口:
这个接口是专门用于供给使用的,其中只有一个get方法用于获取需要的对象。
@FunctionalInterface
public interface Supplier<T> {
/**
* Gets a result.
*
* @return a result
*/
T get(); //实现此方法,实现供给功能
}
public static void main(String[] args) {
Supplier<Student> studentSupplier= Student::new;
studentSupplier.get().hello();
}
public static class Student{
public void hello(){
System.out.println("我是学生");
}
}
Consumer消费型函数式接口:
这个接口专门用于消费某个对象。
@FunctionalInterface
public interface Consumer<T> {
void accept(T t); //这个方法用于消费,没有返回值
default Consumer<T> andThen(Consumer<? super T> after) { //默认实现,这个方法便于我们连续使用此消费接口
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
}
public class Main {
//专门消费Student对象的Consumer
private static final Consumer<Student> STUDENT_COMPARATOR=student->System.out.println(student+"看不懂");
public static void main(String[] args) {
Student student=new Student();
STUDENT_COMPARATOR.accept(student);
}
public static class Student{
public void hello(){
System.out.println("我是学生");
}
}
}
public static void main(String[] args) {
Student student=new Student();
STUDENT_COMPARATOR //可以使用andThen方法继续调用,将消费之后的操作以同样的方式预定好
.andThen(student1 -> System.out.println("后续操作"))
.accept(student);
}
//输出
com.test.Main$Student@404b9385看不懂
后续操作
Function函数型函数式接口:
这个接口消费一个对象,然后会向外供给一个对象(前两个的融合体)
@FunctionalInterface
public interface Function<T, R> {
R apply(T t); //这里一共有两个类型参数,其中一个是接受的参数类型,另一个是返回的结果类型
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
static <T> Function<T, T> identity() {
return t -> t;
}
}
apply方法:
//这里实现了一个简单的功能,将传入的int参数转换为字符串的形式
private static final Function<Integer,String>INTEGER_STRING_FUNCTION=Objects::toString;
//Integer输入,String输出?
public static void main(String[] args) {
String str=INTEGER_STRING_FUNCTION.apply(10);
System.out.println(str);
}
使用compose将指定函数式的结果作为当前函数式的实参:(compose是前置工作)
public static void main(String[] args) {
String str=INTEGER_STRING_FUNCTION
.compose((String s)->s.length()) //将此函数式的返回值作为当前实现的实参
.apply("aaa"); //传入上面函数式需要的参数
System.out.println(str);
//String ->Integer ->String
// aaa -> 3 -> "3"
}
//输出3
andThen可以将当前实现的返回值进行进一步的处理,得到其他类型的值:(后续工作)
public static void main(String[] args) {
Boolean str=INTEGER_STRING_FUNCTION
.andThen(String::isEmpty) //在执行完后,返回值作为参数执行andThen内的函数式,最后得到的结果就是最终的结果了
.apply(10);
System.out.println(str);
}
//输出false
还提供了一个将传入参数原样返回的实现:
public static void main(String[] args) {
Function<String,String>function=Function.identity();
System.out.println(function.apply("aaaa"));
}
//输出aaaa
Predicate断言式函数式接口:
接收一个参数,然后进行自定义并返回一个boolean结果。
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t); //要实现的方法
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
default Predicate<T> negate() {
return (t) -> !test(t);
}
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}
static <T> Predicate<T> isEqual(Object targetRef) {
return (null == targetRef)
? Objects::isNull
: object -> targetRef.equals(object);
}
}
import java.util.function.Predicate;
public class Main {
private static final Predicate<Student> STUDENT_PREDICATE=student -> student.score>=60;
public static void main(String[] args) {
Student student=new Student();
student.score=80;
if (STUDENT_PREDICATE.test(student)){ //test方法的返回值是一个boolean结果
System.out.println("及格了");
}
else {
System.out.println("不及格");
}
}
public static class Student{
int score=100;
public void hello(){
System.out.println("我是学生");
}
}
}
判空包装
判空包装类Optional,这个类可以很有效的处理空指针问题。
public static void main(String[] args) {
test(null);
}
public static void test(String str){ //传入字符串,如果不是空串就打印长度
if(str == null)return; //没有这句若传入null会出现空指针异常错误
if(!str.isEmpty()){
System.out.println("长度为"+str.length());
}
}
用Optional类处理上述问题:
public static void test(String str){
Optional
.ofNullable(str) //将传入的对象包装进Optional中
.ifPresent(s -> System.out.println("长度为"+s.length()));
//如果不为空(ifPresent)则执行这里的Consumer实现
}
其他的一些使用:
//不为空输出字符串,为空...
public static void test(String str){
String s1=Optional
.ofNullable(str)
.orElse("为null的备选情况");
System.out.println(s1);
}
//将包装的类型直接转换为另一种类型
public static void main(String[] args) {
test("aaaaa");
}
public static void test(String str){
Integer i=Optional
.ofNullable(str)
.map(s -> s.length())
.get();
System.out.println(i);
}
//输出5
......