«Серия Java8» загадочная лямбда

задняя часть
«Серия Java8» загадочная лямбда

сенсорный фон

Когда я впервые столкнулся с лямбда-выражениями, я почувствовал, что это штука очень волшебная (высокоуровневая), один() плюс -> может передать кусок кода, в то время я взял на себя код коллеги в проект компании, и я не разбирался в особенностях java8., я тоже был в недоумении, когда читал, а потом быстро прочитал книгу "война java8" и решил написать блог о серии фич java8, что не только углубило мое впечатление, но и поделилось им с вами. Надеюсь, вы дадите мне еще совет 😄.

Что такое лямбда?

Лямбда — это анонимная функция, мы можем понимать лямбда-выражение как фрагмент кода, который можно передать (передача кода как параметра называется параметризацией поведения). Lambda позволяет использовать функции в качестве параметров метода (функции передаются в методы в качестве параметров), для этого необходимо понимать, что такое функциональный интерфейс, который здесь не будет представлен и будет объяснен в следующей статье. .

Во-первых, давайте посмотрим, как выглядит лямбда? Обычное письмо:

new Thread(new Runnable() {
    @Override
    public void run() {
       System.out.println("hello lambda");
    }
}).start();

лямбда пишет:

new Thread(
    () -> System.out.println("hello lambda")
).start();

Как насчет этого? Это кажется очень кратким, да, в этом прелесть лямбды, она может сделать код, который вы пишете, более простым и гибким.

Как написать лямбда?

在这里插入图片描述
Давайте посмотрим на некоторые из приведенных выше диаграмм.Это синтаксис лямбда.Лямбда разделена на три части: список параметров, оператор и тело лямбда. Ниже приведены важные характеристики лямбда-выражений:

  • 可选类型声明:Нет необходимости объявлять тип параметра, компилятор может единообразно идентифицировать значение параметра. То есть (s) -> System.out.println(s) и (String s) -> System.out.println(s) — то же самое. Компилятор выполнит вывод типов, поэтому нет необходимости добавлять типы параметров.
  • 可选的参数圆括号:Параметр не должен определять круглые скобки, но несколько параметров должны определять круглые скобки. Например:
  1. s -> System.out.println(s) Параметру не нужны круглые скобки.
  2. (x, y) -> Integer.compare(y, x) к двум параметрам добавляются круглые скобки, иначе компилятор сообщит об ошибке.
  • 可选的大括号:Если тело содержит оператор, фигурные скобки не требуются.
    1. s -> System.out.println(s) , фигурные скобки не нужны.
    2. (с) -> { если (s.equals("s")){ System.out.println(s); } }; требуются фигурные скобки
  • 可选的返回关键字:Если тело имеет только одно возвращаемое значение выражения, компилятор автоматически вернет значение, а в фигурных скобках нужно указать, что выражение возвращает значение.

Если тело Lambda не добавляет { }, нет необходимости писать return:

 Comparator<Integer> com = (x, y) -> Integer.compare(y, x); 

Добавление { } в тело Lambda требует добавления возврата:

  Comparator<Integer> com = (x, y) -> {
            int compare = Integer.compare(y, x);
            return compare;
        }; 

вывод типа

Выше мы видели, как должно быть написано лямбда-выражение, но важной особенностью лямбда является то, что可选参数类型声明, то есть вам не нужно писать тип параметра, так почему бы не написать его? Как он узнает тип параметра? Это включает вывод типа.

Общие улучшения вывода типов для java8:

  • Поддержка вывода универсального целевого типа из контекста метода.
  • Поддержка вывода универсального типа, передаваемого последнему методу в цепочке вызовов методов.
List<Person> ps = ...
Stream<String> names = ps.stream().map(p -> p.getName());

В приведенном выше коде типом ps является список.<Person>, поэтому возвращаемый тип ps.stream() — Stream<Person>. Метод map() принимает функциональный интерфейс типа Function, где тип T — это тип элемента Stream, которым является Person, а тип R неизвестен. Поскольку целевой тип лямбда-выражения все еще неизвестен после разрешения перегрузки, нам нужно вывести тип R: проверяя тип лямбда-выражения, мы обнаруживаем, что тело лямбда-выражения возвращает String, поэтому тип R — String, и таким образом, map() возвращает Stream<String>. В большинстве случаев компилятор может разрешить правильный тип, но если он не может быть разрешен, нам необходимо:

  • используйте явное лямбда-выражение (предоставив явный тип для параметра p), чтобы предоставить дополнительную информацию о типе
  • Преобразование лямбда-выражения в Function
  • Укажите фактический тип для универсального параметра R. (<String>карта (p -> p.getName ()))

ссылка на метод

Ссылки на методы используются для прямого доступа к существующим методам или конструкторам класса или экземпляра, обеспечивая способ ссылки без выполнения метода. Это более краткое и простое для понимания лямбда-выражение.Когда в лямбда-выражении выполняется только один вызов метода, форма прямого использования ссылки на метод более удобочитаема. Ссылки на методы представлены с помощью оператора "::" с именем класса или экземпляра слева и именем метода справа.(注意:方法引用::右边的方法名是不需要加()的,例:User::getName)

Существует несколько форм ссылок на методы:

  • класс :: статический метод
  • класс :: метод экземпляра
  • объект :: метод экземпляра
例如:
    Consumer<String> consumer = (s) -> System.out.println(s);
等同于:
    Consumer<String> consumer = System.out::println;

例如:
    Function<String, Integer> stringToInteger = (String s) -> Integer.parseInt(s);
等同于:
    Function<String, Integer> stringToInteger = Integer::parseInt;

例如:
    BiPredicate<List<String>, String> contains = (list, element) -> list.contains(element);
等同于:
    BiPredicate<List<String>, String> contains = List::contains;

注意:

  • Список параметров и тип возвращаемого значения вызывающего метода в теле Lambda должны быть согласованы со списком функций и типом возвращаемого значения абстрактного метода в функциональном интерфейсе.
  • Если первый параметр в списке параметров Lambda является вызывающей стороной метода экземпляра, а второй параметр является параметром метода экземпляра, вы можете использовать ClassName::method

Ссылка на конструктор

Формат синтаксиса: имя класса::новый

例如:
    Supplier<User> supplier = ()->new User();

等同于:
    Supplier<User> supplier = User::new;

注意:Метод конструктора, который необходимо вызвать, совпадает со списком параметров абстрактного метода в функциональном интерфейсе.

Как реализована лямбда?

Я давно учился писать лямбда, но в чем ее принцип? Давайте просто посмотрим на пример, чтобы увидеть, что правда:

public class StreamTest {

    public static void main(String[] args) {
        printString("hello lambda", (String s) -> System.out.println(s));

    }

    public static void printString(String s, Print<String> print) {
        print.print(s);
    }
}

@FunctionalInterface
interface Print<T> {
    public void print(T t);
}

Приведенный выше код настраивает функциональный интерфейс, определяет статический метод и затем использует этот функциональный интерфейс для получения параметров. После написания этого класса мы переходим к терминальному интерфейсу javac для компиляции, а затем используем javap (javap — это инструмент защиты от синтаксического анализа, который поставляется с jdk. Его функция заключается в обратном разборе области кода (сборки), соответствующей текущему классу в соответствии с файлом байт-кода класса. инструкция), таблица локальных переменных, таблица исключений и таблица сопоставления смещений строк кода, пул констант и т. д.) для разбора, как показано ниже:

  • Выполните команду javap -p ( -p -private показывает все классы и члены)
    \[外链图片转存失败(img-WRQorife-1563206705415)(./1563196007654.png)\]
    Глядя на рисунок выше, видно, что при компиляции генерируется лямбда-выражение.lambda$main$0Статический метод, этот статический метод реализует логику лямбда-выражения, теперь мы знаем, что исходное лямбда-выражение скомпилировано в статический метод, так как же вызывается этот статический метод? мы продолжаем
  • Выполните команду javap -v -p ( -v -verbose выводит дополнительную информацию)
  public com.lxs.stream.StreamTest();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 7: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: ldc           #2                  // String hello lambda
         2: invokedynamic #3,  0              // InvokeDynamic #0:print:()Lcom/lxs/stream/Print;
         7: invokestatic  #4                  // Method printString:(Ljava/lang/String;Lcom/lxs/stream/Print;)V
        10: return
      LineNumberTable:
        line 10: 0
        line 12: 10

  public static void printString(java.lang.String, com.lxs.stream.Print<java.lang.String>);
    descriptor: (Ljava/lang/String;Lcom/lxs/stream/Print;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_1
         1: aload_0
         2: invokeinterface #5,  2            // InterfaceMethod com/lxs/stream/Print.print:(Ljava/lang/Object;)V
         7: return
      LineNumberTable:
        line 15: 0
        line 16: 7
    Signature: #19                          // (Ljava/lang/String;Lcom/lxs/stream/Print<Ljava/lang/String;>;)V

  private static void lambda$main$0(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: aload_0
         4: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         7: return
      LineNumberTable:
        line 10: 0
}
SourceFile: "StreamTest.java"
InnerClasses:
     public static final #58= #57 of #61; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
BootstrapMethods:
  0: #27 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #28 (Ljava/lang/Object;)V
      #29 invokestatic com/lxs/stream/StreamTest.lambda$main$0:(Ljava/lang/String;)V
      #30 (Ljava/lang/String;)V

Здесь размещена только часть структуры байт-кода, поскольку определение пула констант слишком длинное, оно не вставляется.

InnerClasses:
     public static final #58= #57 of #61; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
BootstrapMethods:
  0: #27 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #28 (Ljava/lang/Object;)V
      #29 invokestatic com/lxs/stream/StreamTest.lambda$main$0:(Ljava/lang/String;)V
      #30 (Ljava/lang/String;)V

Благодаря этой структуре байт-кода обнаруживается, что должен быть сгенерирован внутренний класс, метод LambdaMetafactory.metafactory вызывается с помощью invokestatic, а методlambda$main$0Передано в качестве параметра, давайте посмотрим на код реализации в методе метафабрики:

    public static CallSite metafactory(MethodHandles.Lookup caller,
                                       String invokedName,
                                       MethodType invokedType,
                                       MethodType samMethodType,
                                       MethodHandle implMethod,
                                       MethodType instantiatedMethodType)
            throws LambdaConversionException {
        AbstractValidatingLambdaMetafactory mf;
        mf = new InnerClassLambdaMetafactory(caller, invokedType,
                                             invokedName, samMethodType,
                                             implMethod, instantiatedMethodType,
                                             false, EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY);
        mf.validateMetafactoryArgs();
        return mf.buildCallSite();
    }

В функции buildCallSite именно функция spinInnerClass создает этот внутренний класс. То есть генерируется внутренний класс, такой как StreamTest?Lambda$1.class Этот класс создается во время выполнения и не сохраняется на диске.

    @Override
    CallSite buildCallSite() throws LambdaConversionException {
        final Class<?> innerClass = spinInnerClass();
        以下省略。。。
    }

Если вы хотите увидеть этот встроенный класс, вы можете установить параметры среды с помощью System.setProperty("jdk.internal.lambda.dumpProxyClasses", " . "); Этот внутренний класс будет сгенерирован по указанному вами пути Текущий путь выполнения. Посмотрим, как выглядит сгенерированный класс.

在这里插入图片描述
Из рисунка видно, что динамически сгенерированный внутренний класс реализует мой пользовательский функциональный интерфейс и переписывает методы в функциональном интерфейсе.

Давайте посмотрим на javap -v -p StreamTest?Lambda$1.class:

{
  private com.lxs.stream.StreamTest$$Lambda$1();
    descriptor: ()V
    flags: ACC_PRIVATE
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #10                 // Method java/lang/Object."<init>":()V
         4: return

  public void print(java.lang.Object);
    descriptor: (Ljava/lang/Object;)V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=2, args_size=2
         0: aload_1
         1: checkcast     #15                 // class java/lang/String
         4: invokestatic  #21                 // Method com/lxs/stream/StreamTest.lambda$main$0:(Ljava/lang/String;)V
         7: return
    RuntimeVisibleAnnotations:
      0: #13()
}

Было обнаружено, что метод lambda$main$0 вызывался с помощью директивы invokestatic в переопределенном методе parint.

Суммировать:Таким образом, реализуется лямбда-выражение, используется инструкция invokedynamic, во время выполнения вызывается LambdaMetafactory.metafactory для динамической генерации внутреннего класса, реализуется функциональный интерфейс, а метод в функциональном интерфейсе переписывается, и метод вызывается внутри метода.lambda$main$0, блок вызывающего метода во внутреннем классе не генерируется динамически, но статический метод был скомпилирован и сгенерирован в исходном классе, и внутреннему классу нужно только вызвать статический метод.

После просмотра ставьте лайки и подписывайтесь! Другие блоги будут следовать. Если есть какая-то ошибка, пожалуйста, поправьте меня.