Java 8 NullPointerException di Collectors.toMap

331

Java 8 Collectors.toMapmelempar a NullPointerExceptionjika salah satu nilainya adalah 'nol'. Saya tidak mengerti perilaku ini, peta dapat berisi pointer nol sebagai nilai tanpa masalah. Apakah ada alasan bagus mengapa nilai tidak dapat null Collectors.toMap?

Juga, apakah ada cara Java 8 yang bagus untuk memperbaikinya, atau haruskah saya kembali ke polos lama untuk loop?

Contoh masalah saya:

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;


class Answer {
    private int id;

    private Boolean answer;

    Answer() {
    }

    Answer(int id, Boolean answer) {
        this.id = id;
        this.answer = answer;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public Boolean getAnswer() {
        return answer;
    }

    public void setAnswer(Boolean answer) {
        this.answer = answer;
    }
}

public class Main {
    public static void main(String[] args) {
        List<Answer> answerList = new ArrayList<>();

        answerList.add(new Answer(1, true));
        answerList.add(new Answer(2, true));
        answerList.add(new Answer(3, null));

        Map<Integer, Boolean> answerMap =
        answerList
                .stream()
                .collect(Collectors.toMap(Answer::getId, Answer::getAnswer));
    }
}

Stacktrace:

Exception in thread "main" java.lang.NullPointerException
    at java.util.HashMap.merge(HashMap.java:1216)
    at java.util.stream.Collectors.lambda$toMap$168(Collectors.java:1320)
    at java.util.stream.Collectors$$Lambda$5/1528902577.accept(Unknown Source)
    at java.util.stream.ReduceOps$3ReducingSink.accept(ReduceOps.java:169)
    at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1359)
    at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:512)
    at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:502)
    at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
    at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
    at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499)
    at Main.main(Main.java:48)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:483)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:134)

Masalah ini masih ada di Jawa 11.

Jasper
sumber
5
nullselalu sedikit bermasalah, seperti di TreeMap. Mungkin saat yang tepat untuk mencoba Optional<Boolean>? Kalau tidak, bagi dan gunakan filter.
Joop Eggen
5
@JoopEggen nullbisa menjadi masalah untuk kunci, tetapi dalam hal ini nilainya.
gontard
Tidak semua peta bermasalah null, HashMapmisalnya dapat memiliki satu nullkunci dan sejumlah nullnilai, Anda dapat mencoba membuat custom Collectormenggunakan HashMapalih - alih menggunakan yang default.
kajacx
2
@ kajacx Tetapi implementasi standarnya adalah HashMap- seperti yang ditunjukkan pada baris pertama stacktrace. Masalahnya bukan bahwa tidak Mapdapat menyimpan nullnilai, tetapi argumen Map#mergefungsi kedua tidak boleh nol.
czerny
Secara pribadi, dengan keadaan yang diberikan, saya akan pergi dengan solusi non-stream, atau forEach () jika inputnya paralel. Solusi berbasis arus pendek yang bagus di bawah ini dapat memiliki kinerja yang buruk.
Ondra Žižka

Jawaban:

302

Anda dapat mengatasi bug yang dikenal ini di OpenJDK dengan ini:

Map<Integer, Boolean> collect = list.stream()
        .collect(HashMap::new, (m,v)->m.put(v.getId(), v.getAnswer()), HashMap::putAll);

Ini tidak terlalu cantik, tetapi berhasil. Hasil:

1: true
2: true
3: null

( ini tutorial paling membantu saya.)

kajacx
sumber
3
@ Belok ya, definisi pemasok (argumen pertama) adalah fungsi yang tidak melewati parameter dan mengembalikan hasil, sehingga lambda untuk kasus Anda akan () -> new TreeMap<>(String.CASE_INSENSITIVE_ORDER)membuat case tidak sensitif Stringdikunci TreeMap.
Brett Ryan
2
Ini adalah jawaban yang benar, dan IMHO apa yang seharusnya dilakukan JDK untuk versi non-kelebihan bawaannya. Mungkin menggabungkan lebih cepat, tetapi saya belum diuji.
Brett Ryan
1
Aku harus menentukan jenis parameter untuk mengkompilasi, cara itu: Map<Integer, Boolean> collect = list.stream().collect(HashMap<Integer, Boolean>::new, (m,v)->m.put(v.getId(), v.getAnswer()), HashMap<Integer, Boolean>::putAll);. Saya punya:incompatible types: cannot infer type-variable(s) R (argument mismatch; invalid method reference no suitable method found for putAll(java.util.Map<java.lang.Integer,java.lang.Boolean>,java.util.Map<java.lang.Integer,java.lang.Boolean>) method java.util.Map.putAll(java.util.Map) is not applicable (actual and formal argument lists differ in length)
Anthony O.
2
Ini mungkin sangat lambat pada input yang besar. Anda membuat HashMapdan kemudian memanggil putAll()untuk setiap entri. Secara pribadi, pada keadaan tertentu, saya akan menggunakan solusi non-stream, atau forEach()jika inputnya paralel.
Ondra Žižka
3
Berhati-hatilah karena solusi ini berperilaku berbeda dari implementasi toMap asli. Implementasi asli mendeteksi kunci duplikat dan melempar IllegalStatException, tetapi solusi ini secara diam-diam menerima kunci terbaru. Solusi Emmanuel Touzery ( stackoverflow.com/a/32648397/471214 ) lebih dekat dengan perilaku asli.
mmdemirbas
174

Itu tidak mungkin dengan metode statis Collectors. Javadoc toMapmenjelaskan yang toMapdidasarkan pada Map.merge:

@param mergeFungsi fungsi penggabungan, digunakan untuk menyelesaikan tabrakan antara nilai yang terkait dengan kunci yang sama, seperti yang disediakan untuk Map#merge(Object, Object, BiFunction)}

dan javadoc dari Map.merge mengatakan:

@throws NullPointerException jika kunci yang ditentukan adalah nol dan peta ini tidak mendukung kunci nol atau nilai atau fungsi remappingFungsi adalah nol

Anda dapat menghindari for for dengan menggunakan forEachmetode daftar Anda.

Map<Integer,  Boolean> answerMap = new HashMap<>();
answerList.forEach((answer) -> answerMap.put(answer.getId(), answer.getAnswer()));

tetapi itu tidak benar-benar sederhana dari cara lama:

Map<Integer, Boolean> answerMap = new HashMap<>();
for (Answer answer : answerList) {
    answerMap.put(answer.getId(), answer.getAnswer());
}
gontard
sumber
3
Dalam hal ini saya lebih suka menggunakan yang kuno untuk masing-masing. Haruskah saya menganggap ini sebagai bug di toMerge? karena penggunaan fungsi gabungan ini benar-benar merupakan detail implementasi, atau apakah alasan yang baik untuk tidak mengizinkan toMap memproses nilai-nilai nol?
Jasper
6
Ini ditentukan dalam javadoc gabungan, tetapi tidak disebutkan dalam dokumen toMap
Jasper
119
Tidak pernah berpikir bahwa nilai nol di peta akan berdampak pada API standar, saya lebih suka menganggapnya sebagai cacat.
Askar Kalykov
16
Sebenarnya dokumen API tidak menyatakan apa pun tentang penggunaan Map.merge. IMHO ini adalah kelemahan dalam implementasi yang membatasi kasus penggunaan yang dapat diterima yang telah diabaikan. Metode kelebihan toMapmenyatakan penggunaan Map.mergetapi bukan yang OP gunakan.
Brett Ryan
11
@Jasper bahkan ada bug melaporkan bugs.openjdk.java.net/browse/JDK-8148463
pixel
23

Saya menulis Collectoryang, tidak seperti java standar, tidak crash ketika Anda memiliki nullnilai:

public static <T, K, U>
        Collector<T, ?, Map<K, U>> toMap(Function<? super T, ? extends K> keyMapper,
                Function<? super T, ? extends U> valueMapper) {
    return Collectors.collectingAndThen(
            Collectors.toList(),
            list -> {
                Map<K, U> result = new HashMap<>();
                for (T item : list) {
                    K key = keyMapper.apply(item);
                    if (result.putIfAbsent(key, valueMapper.apply(item)) != null) {
                        throw new IllegalStateException(String.format("Duplicate key %s", key));
                    }
                }
                return result;
            });
}

Cukup ganti Collectors.toMap()panggilan Anda ke panggilan ke fungsi ini dan itu akan memperbaiki masalah.

Emmanuel Touzery
sumber
1
Tetapi membiarkan nullnilai-nilai dan penggunaan putIfAbsenttidak bisa dimainkan bersama. Itu tidak mendeteksi kunci duplikat ketika mereka memetakan ke null...
Holger
10

Ya, jawaban terlambat dari saya, tapi saya pikir mungkin membantu untuk memahami apa yang terjadi di bawah tenda kalau-kalau ada yang ingin kode beberapa lainnya Collector -logic .

Saya mencoba memecahkan masalah dengan mengkode pendekatan yang lebih asli dan lurus ke depan. Saya pikir ini selangsung mungkin:

public class LambdaUtilities {

  /**
   * In contrast to {@link Collectors#toMap(Function, Function)} the result map
   * may have null values.
   */
  public static <T, K, U, M extends Map<K, U>> Collector<T, M, M> toMapWithNullValues(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper) {
    return toMapWithNullValues(keyMapper, valueMapper, HashMap::new);
  }

  /**
   * In contrast to {@link Collectors#toMap(Function, Function, BinaryOperator, Supplier)}
   * the result map may have null values.
   */
  public static <T, K, U, M extends Map<K, U>> Collector<T, M, M> toMapWithNullValues(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper, Supplier<Map<K, U>> supplier) {
    return new Collector<T, M, M>() {

      @Override
      public Supplier<M> supplier() {
        return () -> {
          @SuppressWarnings("unchecked")
          M map = (M) supplier.get();
          return map;
        };
      }

      @Override
      public BiConsumer<M, T> accumulator() {
        return (map, element) -> {
          K key = keyMapper.apply(element);
          if (map.containsKey(key)) {
            throw new IllegalStateException("Duplicate key " + key);
          }
          map.put(key, valueMapper.apply(element));
        };
      }

      @Override
      public BinaryOperator<M> combiner() {
        return (left, right) -> {
          int total = left.size() + right.size();
          left.putAll(right);
          if (left.size() < total) {
            throw new IllegalStateException("Duplicate key(s)");
          }
          return left;
        };
      }

      @Override
      public Function<M, M> finisher() {
        return Function.identity();
      }

      @Override
      public Set<Collector.Characteristics> characteristics() {
        return Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.IDENTITY_FINISH));
      }

    };
  }

}

Dan tes menggunakan JUnit dan menegaskan:

  @Test
  public void testToMapWithNullValues() throws Exception {
    Map<Integer, Integer> result = Stream.of(1, 2, 3)
        .collect(LambdaUtilities.toMapWithNullValues(Function.identity(), x -> x % 2 == 1 ? x : null));

    assertThat(result)
        .isExactlyInstanceOf(HashMap.class)
        .hasSize(3)
        .containsEntry(1, 1)
        .containsEntry(2, null)
        .containsEntry(3, 3);
  }

  @Test
  public void testToMapWithNullValuesWithSupplier() throws Exception {
    Map<Integer, Integer> result = Stream.of(1, 2, 3)
        .collect(LambdaUtilities.toMapWithNullValues(Function.identity(), x -> x % 2 == 1 ? x : null, LinkedHashMap::new));

    assertThat(result)
        .isExactlyInstanceOf(LinkedHashMap.class)
        .hasSize(3)
        .containsEntry(1, 1)
        .containsEntry(2, null)
        .containsEntry(3, 3);
  }

  @Test
  public void testToMapWithNullValuesDuplicate() throws Exception {
    assertThatThrownBy(() -> Stream.of(1, 2, 3, 1)
        .collect(LambdaUtilities.toMapWithNullValues(Function.identity(), x -> x % 2 == 1 ? x : null)))
            .isExactlyInstanceOf(IllegalStateException.class)
            .hasMessage("Duplicate key 1");
  }

  @Test
  public void testToMapWithNullValuesParallel() throws Exception {
    Map<Integer, Integer> result = Stream.of(1, 2, 3)
        .parallel() // this causes .combiner() to be called
        .collect(LambdaUtilities.toMapWithNullValues(Function.identity(), x -> x % 2 == 1 ? x : null));

    assertThat(result)
        .isExactlyInstanceOf(HashMap.class)
        .hasSize(3)
        .containsEntry(1, 1)
        .containsEntry(2, null)
        .containsEntry(3, 3);
  }

  @Test
  public void testToMapWithNullValuesParallelWithDuplicates() throws Exception {
    assertThatThrownBy(() -> Stream.of(1, 2, 3, 1, 2, 3)
        .parallel() // this causes .combiner() to be called
        .collect(LambdaUtilities.toMapWithNullValues(Function.identity(), x -> x % 2 == 1 ? x : null)))
            .isExactlyInstanceOf(IllegalStateException.class)
            .hasCauseExactlyInstanceOf(IllegalStateException.class)
            .hasStackTraceContaining("Duplicate key");
  }

Dan bagaimana Anda menggunakannya? Yah, gunakan saja alih-alihtoMap() seperti tes menunjukkan. Ini membuat kode panggilan terlihat sebersih mungkin.

EDIT:
menerapkan ide Holger di bawah ini, menambahkan metode pengujian

sjngm
sumber
1
Combiner tidak memeriksa kunci duplikat. Jika Anda ingin menghindari memeriksa setiap kunci, Anda dapat menggunakan sesuatu seperti(map1, map2) -> { int total = map1.size() + map2.size(); map1.putAll(map2); if(map1.size() < total.size()) throw new IllegalStateException("Duplicate key(s)"); return map1; }
Holger
@ Holger Yap, itu benar. Terutama karena accumulator()sebenarnya memeriksa itu. Mungkin saya harus melakukan stream paralel sekali :)
sjngm
7

Inilah kolektor yang lebih sederhana daripada yang diusulkan oleh @EmmanuelTouzery. Gunakan jika Anda suka:

public static <T, K, U> Collector<T, ?, Map<K, U>> toMapNullFriendly(
        Function<? super T, ? extends K> keyMapper,
        Function<? super T, ? extends U> valueMapper) {
    @SuppressWarnings("unchecked")
    U none = (U) new Object();
    return Collectors.collectingAndThen(
            Collectors.<T, K, U> toMap(keyMapper,
                    valueMapper.andThen(v -> v == null ? none : v)), map -> {
                map.replaceAll((k, v) -> v == none ? null : v);
                return map;
            });
}

Kami hanya mengganti nulldengan beberapa objek khusus nonedan melakukan operasi terbalik di finisher.

Tagir Valeev
sumber
5

Jika nilainya adalah String, maka ini mungkin berfungsi: map.entrySet().stream().collect(Collectors.toMap(e -> e.getKey(), e -> Optional.ofNullable(e.getValue()).orElse("")))

Gnana
sumber
4
Itu hanya berfungsi jika Anda baik-baik saja dengan memodifikasi data. Metode hilir mungkin mengharapkan nilai nol daripada string kosong.
Sam Buchmiller
3

Menurut Stacktrace

Exception in thread "main" java.lang.NullPointerException
at java.util.HashMap.merge(HashMap.java:1216)
at java.util.stream.Collectors.lambda$toMap$148(Collectors.java:1320)
at java.util.stream.Collectors$$Lambda$5/391359742.accept(Unknown Source)
at java.util.stream.ReduceOps$3ReducingSink.accept(ReduceOps.java:169)
at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1359)
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:512)
at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:502)
at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499)
at com.guice.Main.main(Main.java:28)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:483)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:134)

Kapan disebut map.merge

        BiConsumer<M, T> accumulator
            = (map, element) -> map.merge(keyMapper.apply(element),
                                          valueMapper.apply(element), mergeFunction);

Ini akan melakukan nullpemeriksaan sebagai hal pertama

if (value == null)
    throw new NullPointerException();

Saya tidak menggunakan Java 8 begitu sering sehingga saya tidak tahu apakah ada cara yang lebih baik untuk memperbaikinya, tetapi memperbaikinya agak sulit.

Anda bisa melakukannya:

Gunakan filter untuk memfilter semua nilai NULL, dan dalam kode Javascript periksa apakah server tidak mengirim jawaban untuk id ini berarti ia tidak membalasnya.

Sesuatu seperti ini:

Map<Integer, Boolean> answerMap =
        answerList
                .stream()
                .filter((a) -> a.getAnswer() != null)
                .collect(Collectors.toMap(Answer::getId, Answer::getAnswer));

Atau gunakan mengintip, yang digunakan untuk mengubah elemen aliran untuk elemen. Dengan mengintip, Anda dapat mengubah jawaban untuk sesuatu yang lebih dapat diterima untuk peta, tetapi itu artinya sedikit mengedit logika Anda.

Kedengarannya seperti jika Anda ingin mempertahankan desain saat ini Anda harus menghindari Collectors.toMap

Marco Acierno
sumber
3

Saya telah sedikit memodifikasi implementasi Emmanuel Touzery .

Versi ini;

  • Mengizinkan kunci nol
  • Mengizinkan nilai nol
  • Mendeteksi kunci duplikat (bahkan jika itu nol) dan melempar IllegalStateException seperti dalam implementasi JDK asli.
  • Mendeteksi kunci duplikat juga ketika kunci sudah dipetakan ke nilai nol. Dengan kata lain, pisahkan pemetaan dengan nilai nol dari tanpa pemetaan.
public static <T, K, U> Collector<T, ?, Map<K, U>> toMapOfNullables(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper) {
    return Collectors.collectingAndThen(
        Collectors.toList(),
        list -> {
            Map<K, U> map = new LinkedHashMap<>();
            list.forEach(item -> {
                K key = keyMapper.apply(item);
                if (map.containsKey(key)) {
                    throw new IllegalStateException(String.format("Duplicate key %s", key));
                }
                map.put(key, valueMapper.apply(item));
            });
            return map;
        }
    );
}

Tes unit:

@Test
public void toMapOfNullables_WhenHasNullKey() {
    assertEquals(singletonMap(null, "value"),
        Stream.of("ignored").collect(Utils.toMapOfNullables(i -> null, i -> "value"))
    );
}

@Test
public void toMapOfNullables_WhenHasNullValue() {
    assertEquals(singletonMap("key", null),
        Stream.of("ignored").collect(Utils.toMapOfNullables(i -> "key", i -> null))
    );
}

@Test
public void toMapOfNullables_WhenHasDuplicateNullKeys() {
    assertThrows(new IllegalStateException("Duplicate key null"),
        () -> Stream.of(1, 2, 3).collect(Utils.toMapOfNullables(i -> null, i -> i))
    );
}

@Test
public void toMapOfNullables_WhenHasDuplicateKeys_NoneHasNullValue() {
    assertThrows(new IllegalStateException("Duplicate key duplicated-key"),
        () -> Stream.of(1, 2, 3).collect(Utils.toMapOfNullables(i -> "duplicated-key", i -> i))
    );
}

@Test
public void toMapOfNullables_WhenHasDuplicateKeys_OneHasNullValue() {
    assertThrows(new IllegalStateException("Duplicate key duplicated-key"),
        () -> Stream.of(1, null, 3).collect(Utils.toMapOfNullables(i -> "duplicated-key", i -> i))
    );
}

@Test
public void toMapOfNullables_WhenHasDuplicateKeys_AllHasNullValue() {
    assertThrows(new IllegalStateException("Duplicate key duplicated-key"),
        () -> Stream.of(null, null, null).collect(Utils.toMapOfNullables(i -> "duplicated-key", i -> i))
    );
}
mmdemirbas
sumber
1

Maaf membuka kembali pertanyaan lama, tetapi karena sudah diedit baru-baru ini mengatakan bahwa "masalah" masih ada di Jawa 11, saya merasa ingin menunjukkan ini:

answerList
        .stream()
        .collect(Collectors.toMap(Answer::getId, Answer::getAnswer));

memberi Anda pengecualian penunjuk nol karena peta tidak mengizinkan null sebagai nilai. Ini masuk akal karena jika Anda melihat peta untuk kunci kdan tidak ada, maka nilai yang dikembalikan sudah null(lihat javadoc). Jadi jika Anda bisa memasukkan knilai null, peta akan terlihat seperti berperilaku aneh.

Seperti yang dikatakan seseorang di komentar, cukup mudah untuk menyelesaikan ini dengan menggunakan pemfilteran:

answerList
        .stream()
        .filter(a -> a.getAnswer() != null)
        .collect(Collectors.toMap(Answer::getId, Answer::getAnswer));

dengan cara ini tidak ada nullnilai yang akan dimasukkan dalam peta, dan MASIH akan Anda dapatkannull sebagai "nilai" saat mencari id yang tidak memiliki jawaban di peta.

Saya harap ini masuk akal untuk semua orang.

Luca
sumber
1
Masuk akal jika peta tidak mengizinkan nilai nol, tetapi itu benar. Anda dapat melakukannya answerMap.put(4, null);tanpa masalah. Anda benar bahwa dengan solusi yang diusulkan Anda, Anda akan mendapatkan hasil yang sama untuk anserMap.get () jika tidak ada seolah-olah nilainya akan dimasukkan sebagai nol. Namun, jika Anda mengulangi semua entri peta jelas ada perbedaan.
Jasper
1
public static <T, K, V> Collector<T, HashMap<K, V>, HashMap<K, V>> toHashMap(
        Function<? super T, ? extends K> keyMapper,
        Function<? super T, ? extends V> valueMapper
)
{
    return Collector.of(
            HashMap::new,
            (map, t) -> map.put(keyMapper.apply(t), valueMapper.apply(t)),
            (map1, map2) -> {
                map1.putAll(map2);
                return map1;
            }
    );
}

public static <T, K> Collector<T, HashMap<K, T>, HashMap<K, T>> toHashMap(
        Function<? super T, ? extends K> keyMapper
)
{
    return toHashMap(keyMapper, Function.identity());
}
Igor Zubchenok
sumber
1
upvoting karena ini mengkompilasi. Jawaban yang diterima tidak dikompilasi karena Peta :: putAll tidak memiliki nilai balik.
Taugenichts
0

Mempertahankan semua id id dengan tweak kecil

Map<Integer, Boolean> answerMap = 
  answerList.stream()
            .collect(Collectors.toMap(Answer::getId, a -> 
                       Boolean.TRUE.equals(a.getAnswer())));
Sigirisetti
sumber
Saya pikir ini adalah jawaban terbaik - ini adalah jawaban yang paling ringkas dan memperbaiki masalah NPE.
LConrad
-3

NullPointerException sejauh ini merupakan pengecualian yang paling sering dijumpai (setidaknya dalam kasus saya). Untuk menghindari ini, saya bersikap defensif dan menambahkan banyak cek nol dan akhirnya saya memiliki kode yang membengkak dan jelek. Java 8 memperkenalkan Opsional untuk menangani referensi nol sehingga Anda dapat menentukan nilai nullable dan non-nullable.

Yang mengatakan, saya akan membungkus semua referensi nullable dalam wadah opsional. Kita juga seharusnya tidak merusak kompatibilitas ke belakang juga. Ini kodenya.

class Answer {
    private int id;
    private Optional<Boolean> answer;

    Answer() {
    }

    Answer(int id, Boolean answer) {
        this.id = id;
        this.answer = Optional.ofNullable(answer);
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    /**
     * Gets the answer which can be a null value. Use {@link #getAnswerAsOptional()} instead.
     *
     * @return the answer which can be a null value
     */
    public Boolean getAnswer() {
        // What should be the default value? If we return null the callers will be at higher risk of having NPE
        return answer.orElse(null);
    }

    /**
     * Gets the optional answer.
     *
     * @return the answer which is contained in {@code Optional}.
     */
    public Optional<Boolean> getAnswerAsOptional() {
        return answer;
    }

    /**
     * Gets the answer or the supplied default value.
     *
     * @return the answer or the supplied default value.
     */
    public boolean getAnswerOrDefault(boolean defaultValue) {
        return answer.orElse(defaultValue);
    }

    public void setAnswer(Boolean answer) {
        this.answer = Optional.ofNullable(answer);
    }
}

public class Main {
    public static void main(String[] args) {
        List<Answer> answerList = new ArrayList<>();

        answerList.add(new Answer(1, true));
        answerList.add(new Answer(2, true));
        answerList.add(new Answer(3, null));

        // map with optional answers (i.e. with null)
        Map<Integer, Optional<Boolean>> answerMapWithOptionals = answerList.stream()
                .collect(Collectors.toMap(Answer::getId, Answer::getAnswerAsOptional));

        // map in which null values are removed
        Map<Integer, Boolean> answerMapWithoutNulls = answerList.stream()
                .filter(a -> a.getAnswerAsOptional().isPresent())
                .collect(Collectors.toMap(Answer::getId, Answer::getAnswer));

        // map in which null values are treated as false by default
        Map<Integer, Boolean> answerMapWithDefaults = answerList.stream()
                .collect(Collectors.toMap(a -> a.getId(), a -> a.getAnswerOrDefault(false)));

        System.out.println("With Optional: " + answerMapWithOptionals);
        System.out.println("Without Nulls: " + answerMapWithoutNulls);
        System.out.println("Wit Defaults: " + answerMapWithDefaults);
    }
}
TriCore
sumber
1
jawaban tidak berguna, mengapa Anda harus menyingkirkan nol untuk memperbaikinya? Ini masalah Collectors.toMap()bukan nilai null
Enerccio
@Enerccio tenang sobat !! Mengandalkan nilai nol bukan praktik yang baik. Jika Anda telah menggunakan Opsional, Anda tidak akan menemukan NPE sejak awal. Baca tentang penggunaan opsional.
TriCore
1
dan mengapa demikian? Nilai kosong baik-baik saja, itu adalah perpustakaan tidak berdokumen yang menjadi masalah. Opsional bagus tetapi tidak di mana-mana.
Enerccio