Как найти лямбду, когда происходит исключение?

Скажем, у меня есть ComparatorFactory, у него есть много компараторов, составленных лямбдой:

   public static Comparator<SomeClass> getXCmp() {
      return (o1, o2) -> {
         Double d1 = Double.parseDouble(o1.getX());
         Double d2 = Double.parseDouble(o2.getX());
         return d1.compareTo(d2);
      };
   }

Я использовал эти компараторы для сортировки и фильтрации данных. К сожалению, я использовал неверный компаратор в каком-то месте, и это вызвало ClassCastException, как показано:

java.lang.ClassCastException: java.lang.Double cannot be cast to java.lang.String
at businesslogic.utility.ComparatorFactory$$Lambda$24/115669291.compare(Unknown Source)
at javax.swing.DefaultRowSorter.compare(DefaultRowSorter.java:968)
...
...

Как вы можете видеть, это показывает (Unknown Source), из-за чего мне трудно найти, какой компаратор ошибочен. Я также попытался добавить точку останова до того, как произойдет сравнение (т.е. В верхнем примере, в DefaulRowSorter.java:968), но next step также не может определить, что это лямбда (он перескакивает на неверный компаратор, который не имеет ничего общего с double и string, и когда я, наконец, нашел ошибку, это было неверно).

После того, как я нашел ошибку (пытаясь понять весь проект и много времени), я попробовал анонимный класс. Ядро стека явно сообщило мне, где оно находится.

Q

Если я хочу, чтобы лямбда предоставила сжатый код, есть ли хороший способ найти, где находится источник лямбда или какая-либо хорошая практика, чтобы помочь мне, когда произойдет исключение?

A простой пример, чтобы повторно создать аналогичную проблему.

Ответы

Ответ 1

Обязательно включите этот параметр для javac при компиляции ваших классов:

-g:lines,source,vars

Опция компилятора "-g" может использоваться для управления количеством отладочной информации, которая должна быть сгенерирована в файлы классов (см. документация)

Вот простой пример с lambdas:

package test;

import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class TestLambda {

    public static Comparator<String> comparator1() {
        return (o1, o2) -> {
            return o1.compareTo(o2);
        };
    }

    public static Comparator<String> comparator2() {
        return (o1, o2) -> {
            System.out.println("test");
            if (true) {
                throw new RuntimeException("Exception"); // line 20: stacktrace points to this line
            }
            return o1.compareTo(o2);
        };
    }

    public static void main(String[] args) {
        List<String> strings = Arrays.asList("string1", "string2", "string3");

        Collections.sort(strings, comparator2());
    }
}

Вот стек:

Exception in thread "main" java.lang.RuntimeException: Exception
    at test.TestLambda.lambda$comparator2$1(TestLambda.java:20)
    at test.TestLambda$$Lambda$1/189568618.compare(Unknown Source)
    at java.util.TimSort.countRunAndMakeAscending(TimSort.java:351)
    at java.util.TimSort.sort(TimSort.java:216)
    at java.util.Arrays.sort(Arrays.java:1438)
    at java.util.Arrays$ArrayList.sort(Arrays.java:3895)
    at java.util.Collections.sort(Collections.java:175)
    at test.TestLambda.main(TestLambda.java:29)

Как вы видите, stacktrace at test.TestLambda.lambda$comparator2$1(TestLambda.java:20) указывает на точную строку исходного кода.

В вашей среде IDE должна быть возможность проанализировать stacktrace и украсить ее ссылками, которые нажимают на которые должны привести вас к точной строке в ваших источниках (по крайней мере, то, что делает IntelliJ IDEA).

Если вы скомпилируете с помощью -g:none, stacktrace будет отличаться:

Exception in thread "main" java.lang.RuntimeException: Exception
    at test.TestLambda.lambda$comparator2$1(Unknown Source)
    at test.TestLambda$$Lambda$1/189568618.compare(Unknown Source)
    at java.util.TimSort.countRunAndMakeAscending(TimSort.java:351)
    at java.util.TimSort.sort(TimSort.java:216)
    at java.util.Arrays.sort(Arrays.java:1438)
    at java.util.Arrays$ArrayList.sort(Arrays.java:3895)
    at java.util.Collections.sort(Collections.java:175)
    at test.TestLambda.main(Unknown Source)

Update:

Ниже приведен еще один пример, который ближе к заданному в вопросе:

package test;

import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class TestLambda {

    public static Comparator<String> comparator1() {
        return (o1, o2) -> {
            return o1.compareTo(o2);
        };
    }

    public static Comparator<String> comparator2() {
        return (o1, o2) -> {
            System.out.println("test");
            if (true) {
                throw new RuntimeException("Exception");
            }
            return o1.compareTo(o2);
        };
    }

    public static void main(String[] args) {
        List strings = Arrays.asList(1, 2, 3);

        Collections.sort(strings, comparator2());
    }
}

Единственное различие заключается в том, что он использует исходный тип для списка, что позволяет использовать компаратор String для списка Integers. Столбец на самом деле не содержит номера строк, поскольку исключение произошло во время кастинга, а не в нашем исходном коде:

Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
    at test.TestLambda$$Lambda$1/189568618.compare(Unknown Source)
    at java.util.TimSort.countRunAndMakeAscending(TimSort.java:351)
    at java.util.TimSort.sort(TimSort.java:216)
    at java.util.Arrays.sort(Arrays.java:1438)
    at java.util.Arrays$ArrayList.sort(Arrays.java:3895)
    at java.util.Collections.sort(Collections.java:175)
    at test.TestLambda.main(TestLambda.java:29)

Основное правило здесь - не использовать необработанные типы, которые в этом случае упростят процесс отладки (Что такое необработанный тип и почему мы не должны его использовать?). Компилятор также может помочь вам здесь: включить этот параметр для javac:

-Xlint:all

Компилятор будет предупреждать вас о сырых типах множеством других вещей. Добавьте еще один вариант:

-Werror

и компилятор выдаст ошибку вместо предупреждения (полезно при использовании с серверами CI для обеспечения высокого качества исходного кода)

Ответ 2

Насколько я пробовал и искал, вы не можете найти где лямбда в Java 8.

Лямбда здесь является заменой анонимного класса, но замена невидима для JVM, поэтому JVM не может найти лямбда.

В качестве примеров возьмем два простых компаратора:

    public static Comparator<String> comparator1() {
    return (o1, o2) -> {
        Double d1 = Double.parseDouble(o1);
        Double d2 = Double.parseDouble(o2);
        return d1.compareTo(d2);
    };
    }

    public static Comparator<String> comparator2() {
    return new Comparator<String>() {
        @Override
        public int compare(String o1, String o2) {
            Double d1 = Double.parseDouble(o1);
            Double d2 = Double.parseDouble(o2);
            return d1.compareTo(d2);
        }
    };
    }

Скомпилирован из примера верхнего кода (удалите некоторые избыточные строки):

public static comparator1()Ljava/util/Comparator;
    INVOKEDYNAMIC compare()Ljava/util/Comparator; [
      // handle kind 0x6 : 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;
      // arguments:
      (Ljava/lang/Object;Ljava/lang/Object;)I, 
      // handle kind 0x6 : INVOKESTATIC
      lambda/ComparatorFa.lambda$comparator1$0(Ljava/lang/String;Ljava/lang/String;)I, 
      (Ljava/lang/String;Ljava/lang/String;)I
    ]

public static comparator2()Ljava/util/Comparator;
    NEW lambda/ComparatorFa$1
    DUP
    INVOKESPECIAL lambda/ComparatorFa$1.<init> ()V

Очень важное различие заключается в том, что второй компаратор имеет класс и NEW экземпляр, но тот, у кого лямбда, просто становится методом, который INVOKEDYNAMIC.

И мы обнаруживаем, что компилятор просто компилирует synthetic method, чтобы JVM мог вызвать:

  private static synthetic lambda$comparator1$0(Ljava/lang/String;Ljava/lang/String;)I
    ALOAD 0
    INVOKESTATIC java/lang/Double.parseDouble (Ljava/lang/String;)D
    INVOKESTATIC java/lang/Double.valueOf (D)Ljava/lang/Double;
    ASTORE 2

    ALOAD 1
    INVOKESTATIC java/lang/Double.parseDouble (Ljava/lang/String;)D
    INVOKESTATIC java/lang/Double.valueOf (D)Ljava/lang/Double;
    ASTORE 3

    ALOAD 2
    ALOAD 3
    INVOKEVIRTUAL java/lang/Double.compareTo (Ljava/lang/Double;)I
    IRETURN

Итак, JVM полностью не знает о существовании lambda. Он просто вызывает метод, когда это необходимо, и он явно не может найти, где находится эта лямбда, поэтому он должен показывать Unknown source.