JAVA 学习 面试(九)Lambda表达式与泛型

Lambda表达式
// 使用 Lambda 表达式计算两个数的和
MathOperation addition = (a, b) -> a + b;
// 调用 Lambda 表达式
int result = addition.operation(5, 3);
// MathOperation 是一个函数式接口,它包含一个抽象方法 operation,Lambda 表达式 (a, b) -> a + b 实现了这个抽象方法,表示对两个参数进行相加操作。
// 在 Lambda 表达式当中不允许声明一个与局部变量同名的参数或者局部变量。
Lambda表达式与匿名内部类:
  • 内部类编译之后将会产生两个 class 文件(编译器自动命名Test$1.class),lambda编译后只会产生一个文件 (LambdaTest.class),即 Lambda 表达式不会产生新的类
  • Lambda 表达式中使用 this 关键字时,其指向的是外部类的引用(因为其不会创建匿名内部类)
函数式接口

函数式接口是为使现有的函数友好地支持Lambda表达式而提出的概念

@FunctionalInterface // Java 8为函数式接口引入了一个新注解@FunctionalInterface,主要用于编译级错误检查,加上该注解,当你写的接口不符合函数式接口定义的时候,编译器会报错。
interface GreetingService {
  void sayMessage(String message);
}
GreetingService greetService1 = message -> System.out.println("Hello " + message);
  • 函数式接口只定义了唯一的抽象方法的接口(除了隐含的Object对象的公共方法),若增加新的方法,就非函数式接口,编译会失败

  • 函数式接口里还允许定义 java.lang.Object 里的 public 方法,因为任何一个函数式接口的实现,默认都继承了 Object 类,包含了来自 java.lang.Object 里对这些抽象方法的实现

  • 可以在接口中添加默认方法与静态方法,无论几个

@FunctionalInterface
public interface XttblogService{
    void sayMessage(String message);
  
    default void doSomeMoreWork1(){//增加默认方法(default),static和default不能同时使用
    }
    static void printHello(){  //增加静态方法(static)
      System.out.println("Hello");
    }
    @Override //Object类中的public方法
    String toString();
}
// 典型的函数式接口:
java.lang.Runnable
java.util.concurrent.Callable
java.util.Comparator
方法引用

可以直接引用已有Java类或对象的方法或构造器。方法引用与lambda表达式结合使用,可以进一步简化代码。

new Random().ints(10)
        .map(i->Math.abs(i))
        .forEach(i -> System.out.println(i));
//其中 i -> Math.abs(i)代理了如下代码:
new IntUnaryOperator() {
    @Override
    public int applyAsInt(int operand) {
        return Math.abs(operand);
    }
}
// 通过方法引用进一步简化:
new Random().ints(10)
        .map(Math::abs)
        .forEach(System.out::println);

四种方法引用:

种类 语法 例子
静态方法引用 ContainingClass::staticMethodName Math::abs
特定对象的实例方法引用 containingObject::instanceMethodName this::equals
实例方法的任意一个特定类型的对象引用 ContainingClass::staticMethodName String::concat
构造器引用 ClassName::new HashSet::new
@FunctionalInterface // 上表中的3
interface TestInterface<T> {
    String handleString(T a, String b);
}
class TestClass {
    String oneString;
    public String concatString(String a) {
        return this.oneString + a;
    }
    public String startHandleString(TestInterface<TestClass> testInterface, String str) {
        String result = testInterface.handleString(this, str);
        return result;
    }
}

public class Test {
    public static void main(String[] args) {
        TestClass testClass = new TestClass();
        testClass.oneString = "abc";
        String result = testClass.startHandleString(TestClass::concatString, "123");
        System.out.println(result);

        //相当于以下效果
        TestClass testClass2 = new TestClass();
        testClass2.oneString = "abc";
        TestInterface theOne2 = (a, b) -> testClass2.concatString(b);
        String result2 = theOne2.handleString(theOne2, "123");
        System.out.println(result2);

    }
}
泛型ref
E - Element (在集合中使用,因为集合中存放的是元素)
 T - Type(Java 类)
 K - Key(键)
 V - Value(值)
 N - Number(数值类型)

**泛型类:**通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:List、Set、Map。

T可以随便写为任意标识,常见的如T、E、K、V等形式的参数

//在实例化泛型类时,必须指定T的具体类型
public class Generic<T>{
    //在类中声明的泛型整个类里面都可以用,除了静态部分,因为泛型是实例化时声明的。
    //静态区域的代码在编译时就已经确定,只与类相关
    class A <E>{
        T t;
    }
    //类里面的方法或类中再次声明同名泛型是允许的,并且该泛型会覆盖掉父类的同名泛型T
    class B <T>{
        T t;
    }
    //静态内部类也可以使用泛型,实例化时赋予泛型实际类型
    static class C <T> {
        T t;
    }
    public static void main(String[] args) {
        //报错,不能使用T泛型,因为泛型T属于实例不属于类
        T t = null;
    }
    public static <T> void show(T t){
 
    }
    //key这个成员变量的类型为T,T的类型由外部指定
    private T key;
 
    public Generic(T key) { //泛型构造方法形参key的类型也为T,T的类型由外部指定
        this.key = key;
    }
 
    public T getKey(){ //泛型方法getKey的返回值类型为T,T的类型由外部指定
        return key;
    }
}

泛型类,是在实例化类的时候指明泛型的具体类型;

public class Generic<T> {
    private T key;
    public Generic(T key) {
        this.key = key;
    }
    public T getKey(){
        return key;
    }
}
// 当创建一个 Generic< T > 类对象时,会向尖括号 <> 中传入具体的数据类型。
@Test
public void test() {
    Generic<String> generic = new Generic<>();// 传入 String 类型
    // <> 中什么都不传入,等价于 Generic generic = new Generic<>();
    Generic generic = new Generic();
}
// 传入 String 类型时,原泛型类可以想象它会自动扩展,其类型参数会被替换:
public class Generic { 
    private String key;
    public Generic(String key) { 
        this.key = key;
    }
    public String getKey() { 
        return key;
    }
}

泛型方法,是在调用方法的时候指明泛型的具体类型 ,当在一个方法签名中的返回值前面声明了一个 < T > 时,该方法就被声明为一个泛型方法。< T >表明该方法声明了一个类型参数 T,并且这个类型参数 T 只能在该方法中使用。当然,泛型方法中也可以使用泛型类中定义的泛型参数。

public class Test<U> {
    // 该方法只是使用了泛型类定义的类型参数,不是泛型方法
    public void testMethod(U u){
        System.out.println(u);
    }
    //  真正声明了下面的方法是一个泛型方法, < T >表明该方法声明了一个类型参数 T,并且这个类型参数 T 只能在该方法中使用。为了避免混淆,如果在一个泛型类中存在泛型方法,那么两者的类型参数最好不要同名。
    public <T> T testMethod1(T t){
        return t;
    }
}
// 泛型方法中可以同时声明多个类型参数。
public class TestMethod<U> {
    public <T, S> T testMethod(T t, S s) {
        return null;
    }
}

public class Test2<T> {
    // 泛型类定义的类型参数 T 不能在静态方法中使用
    // 但可以将静态方法声明为泛型方法,方法中便可以使用其声明的类型参数了
    public static <E> E show(E one) {
        return null;
    }
}

当调用泛型方法时,根据外部传入的实际对象的数据类型,编译器就可以判断出类型参数 T所代表的具体数据类型。

public class Demo {
    public static void main(String args[]) {
        GenericMethod d = new GenericMethod(); // 创建 GenericMethod 对象  

        String str = d.fun("汤姆"); // 给GenericMethod中的泛型方法传递字符串  
        int i = d.fun(30);  // 给GenericMethod中的泛型方法传递数字,自动装箱  
        System.out.println(str); // 输出 汤姆
        System.out.println(i);  // 输出 30

        GenericMethod.show("Lin");// 输出: 静态泛型方法 Lin
    }
}

class GenericMethod {
    // 普通的泛型方法
    public <T> T fun(T t) { // 可以接收任意类型的数据
        return t;
      }

    // 静态的泛型方法
    public static <E> void show(E one){
         System.out.println("静态泛型方法 " + one);
    }
}
  • 当泛型方法的形参列表中有多个类型参数时,在不指定类型参数的情况下,方法中声明的的类型参数为泛型方法中的几种类型参数的共同父类的最小级,直到 Object。
  • 在指定了类型参数的时候,传入泛型方法中的实参的数据类型必须为指定数据类型或者其子类。
public class Test {

    // 这是一个简单的泛型方法
    public static <T> T add(T x, T y) {
        return y;
    }

    public static void main(String[] args) {
        // 一、不显式地指定类型参数
        //(1)传入的两个实参都是 Integer,所以泛型方法中的 ==  
        int i = Test.add(1, 2);

        //(2)传入的两个实参一个是 Integer,另一个是 Float,
        // 所以取共同父类的最小级, == 
        Number f = Test.add(1, 1.2);

        // 传入的两个实参一个是 Integer,另一个是 String,
        // 所以取共同父类的最小级, == 
        Object o = Test.add(1, "asd");
        // 二、显式地指定类型参数
        //(1)指定了 = ,所以传入的实参只能为 Integer 对象
        int a = Test.<Integer>add(1, 2);

        //(2)指定了 = ,所以不能传入 Float 对象
        int b = Test.<Integer>add(1, 2.2);// 编译错误
        //(3)指定 = ,所以可以传入 Number 对象
        // Integer 和 Float 都是 Number 的子类,因此可以传入两者的对象
        Number c = Test.<Number>add(1, 2.2);
    }
}

类型擦除

泛型的本质是将数据类型参数化,它通过擦除的方式来实现,即编译器会在编译期间擦除代码中的所有泛型语法并相应的做出一些类型转换动作。

换而言之,泛型信息只存在于代码编译阶段,在代码编译结束后,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。也就是说,成功编译过后的 class 文件中不包含任何泛型信息,泛型信息不会进入到运行时阶段。

public class GenericType {
    public static void main(String[] args) {
        ArrayList<String> arrayString = new ArrayList<String>();
        ArrayList<Integer> arrayInteger = new ArrayList<Integer>();
        System.out.println(arrayString.getClass() == arrayInteger.getClass());// true
    }
}
// 在编译期间,所有的泛型信息都会被擦除, ArrayList< Integer > 和 ArrayList< String >类型,在编译后都会变成ArrayList< Object>类型。
public class Caculate<T> {
    private T num;
}//变为:
public class Caculate {
    public Caculate() {}// 默认构造器,不用管
    private Object num;// T 被替换为 Object 类型
}

public class Caculate<T extends Number> {
    private T num;
}// 变为:
public class Caculate {
    public Caculate() {}// 默认构造器,不用管

    private Number num;
}
  1. 泛型信息(包括泛型类、接口、方法)只在代码编译阶段存在,在代码成功编译后,其内的所有泛型信息都会被擦除,并且类型参数 T 会被统一替换为其原始类型(默认是 Object 类,若有 extends 或者 super 则另外分析);
  2. 在泛型信息被擦除后,若还需要使用到对象相关的泛型信息,编译器底层会自动进行类型转换(从原始类型转换为未擦除前的数据类型)。
通配符

通配符?是一种特殊的泛型类型,用于限定泛型类型参数的范围,在java中,数组是可以协变的,比如dog extends Animal,那么Animal[] 与dog[]是兼容的。而集合是不能协变的,也就是说List不是List的父类,这时候就可以用到通配符了。

限定通配符包括两种:
\1. 表示类型的上界,格式为:<? extends T>,即类型必须为T类型或者T子类
\2. 表示类型的下界,格式为:<? super T>,即类型必须为T类型或者T的父类

public class Test { //上界
	public static void printIntValue(List<? extends Number> list) {
    l.add(1); // 编译报错,不能使用add方法,因为我们在写这个方法时不能确定传入的什么类型的数据
		for (Number number : list) {
			System.out.print(number.intValue()+" "); 
		}
		System.out.println();
	}
	public static void main(String[] args) {
		List<Integer> integerList=new ArrayList<Integer>();
		integerList.add(2);
		integerList.add(2);
		printIntValue(integerList);
		List<Float> floatList=new ArrayList<Float>();
		floatList.add((float) 3.3);
		floatList.add((float) 0.3);
		printIntValue(floatList);
	}
}
// 下界
public static void addNumbers(List<? super Integer> list) {
    for (int i = 1; i <= 10; i++) {
        list.add(i); // 能够调用add方法, 因为这里元素就是Integer类型的, 而传入的list不管是什么, 都一定是Integer或其父类泛型的List
    }
}
public static void main(String[] args) {
    List<Object> list1 = new ArrayList<>();
    addNumbers(list1);
    System.out.println(list1);
    List<Number> list2 = new ArrayList<>();
    addNumbers(list2);
    System.out.println(list2);
    List<Double> list3 = new ArrayList<>();
    // addNumbers(list3); // 编译报错
}
// 所传入的数据类型可能是Integer到Object之间的任何类型, 这是无法预料的, 也就无法接收. 唯一能确定的就是Object, 因为所有类型都是其子类型.
public static void getTest2(List<? super Integer> list) {
    // Integer i = list.get(0); //编译报错
    Object o = list.get(1);
}

下界通配符的另一个常见用法:TreeSet t = TreeSet(Comparator comparator) ,在Comparator中可以利用类E的任意一个父类的属性进行排序,只有元素是?的子类型的TreeSet才可以应用这个comparator,如下:

public class Person {
    private String name;
    private int age;
    /*
     * 构造函数与getter, setter省略
     */
}

public class Student extends Person {
    public Student() {}

    public Student(String name, int age) {
        super(name, age);
    }
}

class comparatorTest implements Comparator<Person>{
    @Override
    public int compare(Student s1, Student s2) {
        int num = s1.getAge() - s2.getAge();
        return num == 0 ? s1.getName().compareTo(s2.getName()) :  num;
    }
}

public class GenericTest {
    public static void main(String[] args) {
        TreeSet<Person> ts1 = new TreeSet<>(new comparatorTest());
        ts1.add(new Person("Tom", 20));
        ts1.add(new Person("Jack", 25));
        ts1.add(new Person("John", 22));
        System.out.println(ts1);

        TreeSet<Student> ts2 = new TreeSet<>(new comparatorTest());
        ts2.add(new Student("Susan", 23));
        ts2.add(new Student("Rose", 27));
        ts2.add(new Student("Jane", 19));
        System.out.println(ts2);
    }
}

非限定通配符:无边界的通配符(Unbounded Wildcards), 就是, 比如List,无边界的通配符的主要作用就是让泛型能够接受未知类型的数据.

/* 这种使用List的方式就是父类引用指向子类对象. 注意, 这里不能printList(List list)的形式, , 虽然Object类是所有类的父类, 但是List跟其他泛型的List如List, List不存在继承关系,  */
public static void printList(List<?> list) {
    for (Object o : list) {
        System.out.println(o);
    }
}

public static void main(String[] args) {
    List<String> l1 = new ArrayList<>();
    l1.add("aa");
    l1.add("bb");
    l1.add("cc");
    printList(l1);
    List<Integer> l2 = new ArrayList<>();
    l2.add(11);
    l2.add(22);
    l2.add(33);
    printList(l2);
}
// 我们不能对List使用add方法, 仅有一个例外, 就是add(null). 为什么呢? 因为我们不确定该List的类型, 不知道add什么类型的数据才对, 只有null是所有引用数据类型都具有的元素
public static void addTest(List<?> list) {
    Object o = new Object();
    list.add(o); // 编译报错
    list.add(1); // 编译报错
    list.add("ABC"); // 编译报错
    list.add(null);
}
// Object是所有数据类型的父类, 所以只有接受他可以
public static void getTest(List<?> list) {
    String s = list.get(0); // 编译报错
    Integer i = list.get(1); // 编译报错
    Object o = list.get(2);
}

因为我们不知道c的元素类型,我们不能向其中添加对象。add方法有类型参数E作为集合的元素类型。我们传给add的任何参数都必须是一个未知类型的子类。因为我们不知道那是什么类型,所以我们无法传任何东西进去。

常见泛型使用错误
不可以给Array加泛型
Array<Integer>,//Type Array does not have type parameters
//而使用通配符创建泛型数组是可以的,如下面这个例子:
List<?>[] ls = new ArrayList<?>[10];  

Vector<String> v = new Vector<Object>(); //错误!///不写没错,写了就是明知故犯

Vector<Object> v = new Vector<String>(); //也错误!

你可能感兴趣的:(java,学习,面试)