Java 8 NullPointerException ใน Collector.toMap


331

Java 8 จะส่งCollectors.toMapa NullPointerExceptionหากค่าใดค่าหนึ่งเป็น 'null' ฉันไม่เข้าใจพฤติกรรมนี้แผนที่สามารถมีพอยน์เตอร์พอยน์เตอร์เป็นค่าโดยไม่มีปัญหาใด ๆ มีเหตุผลที่ดีหรือไม่ที่ค่าต่างๆจะไม่เป็นค่าว่างCollectors.toMap?

นอกจากนี้ยังมีวิธีที่ดีของ Java 8 ในการแก้ไขปัญหานี้หรือฉันควรเปลี่ยนกลับเป็น old old for loop หรือไม่?

ตัวอย่างของปัญหาของฉัน:

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)

ปัญหานี้ยังคงมีอยู่ใน Java 11


5
nullเป็นปัญหาเล็กน้อยเช่นใน TreeMap อาจจะเป็นช่วงเวลาที่ดีที่จะลองOptional<Boolean>? มิฉะนั้นแยกและใช้ตัวกรอง
Joop Eggen

5
@JoopEggen nullอาจเป็นปัญหาสำหรับคีย์ แต่ในกรณีนี้คือค่า
gontard

แผนที่ทั้งหมดไม่ได้มีปัญหากับnull, HashMapตัวอย่างเช่นสามารถมีหนึ่งnullที่สำคัญและจำนวนใด ๆnullค่าคุณสามารถลองสร้างที่กำหนดเองCollectorโดยใช้HashMapแทนการใช้หนึ่งเริ่มต้น
kajacx

2
@kajacx แต่การใช้งานเริ่มต้นคือHashMap- ดังที่แสดงในบรรทัดแรกของสแต็คเทรซ ปัญหาไม่Mapได้อยู่ที่nullค่าคงค้างไม่ได้แต่อาร์กิวเมนต์ที่สองของMap#mergeฟังก์ชันไม่สามารถเป็นค่าว่างได้
czerny

โดยส่วนตัวแล้วในสถานการณ์ที่กำหนดฉันจะไปกับโซลูชันที่ไม่ใช่สตรีมหรือ forEach () ถ้าอินพุตขนาน การแก้ปัญหาแบบสตรีมสั้น ๆ ที่ดีด้านล่างอาจมีประสิทธิภาพแย่มาก
Ondra Žižka

คำตอบ:


301

คุณสามารถแก้ไขข้อผิดพลาดที่รู้จักใน OpenJDK ด้วยสิ่งนี้:

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

มันไม่ได้สวยขนาดนั้น แต่ได้ผล ผลลัพธ์:

1: true
2: true
3: null

( บทช่วยสอนนี้ช่วยฉันได้มากที่สุด)


3
@Jagger ใช่ความหมายของการเป็นผู้จัดจำหน่าย (อาร์กิวเมนต์แรก) เป็นฟังก์ชั่นที่ส่งผ่านพารามิเตอร์และไม่มีผลตอบแทนจึงแลมบ์ดาสำหรับกรณีของคุณจะเป็น() -> new TreeMap<>(String.CASE_INSENSITIVE_ORDER)การสร้างกรณีตายเครียดString TreeMap
Brett Ryan

2
นี่คือคำตอบที่ถูกต้องและ IMHO สิ่งที่ JDK ควรทำกับรุ่นเริ่มต้นที่ไม่ใช่โอเวอร์โหลดแทน อาจจะรวมเร็วกว่า แต่ฉันยังไม่ได้ทดสอบ
Brett Ryan

1
ฉันต้องระบุพารามิเตอร์ประเภทเพื่อที่จะรวบรวมวิธีนั้น: Map<Integer, Boolean> collect = list.stream().collect(HashMap<Integer, Boolean>::new, (m,v)->m.put(v.getId(), v.getAnswer()), HashMap<Integer, Boolean>::putAll);. ฉันมี: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)
แอนโทนี่โอ

2
นี่อาจจะค่อนข้างช้าในอินพุตขนาดใหญ่ คุณสร้างHashMapแล้วโทรputAll()ทุกรายการเดียว โดยส่วนตัวแล้วในสถานการณ์ที่กำหนดฉันจะไปกับโซลูชันที่ไม่ใช่กระแสข้อมูลหรือforEach()หากอินพุตนั้นขนาน
Ondra Žižka

3
ระวังว่าวิธีนี้จะทำงานแตกต่างจากการใช้แผนที่ toMap ดั้งเดิม การใช้งานดั้งเดิมตรวจพบคีย์ที่ซ้ำกันและส่ง IllegalStatException แต่โซลูชันนี้ยอมรับคีย์ล่าสุดอย่างเงียบ ๆ วิธีแก้ปัญหาของ Emmanuel Touzery ( stackoverflow.com/a/32648397/471214 ) ใกล้เคียงกับพฤติกรรมดั้งเดิม
mmdemirbas

174

Collectorsมันเป็นไปไม่ได้กับวิธีการคงที่ของ javadoc ของtoMapคำอธิบายนั้นtoMapมีพื้นฐานมาจากMap.merge:

@param merge ฟังก์ชั่นการผสานใช้เพื่อแก้ไขการชนกันระหว่างค่าที่เกี่ยวข้องกับคีย์เดียวกันตามที่กำหนดให้ Map#merge(Object, Object, BiFunction)}

และ javadoc ของMap.mergeพูดว่า:

@throws NullPointerException ถ้าคีย์ที่ระบุเป็นโมฆะและแผนที่นี้ไม่สนับสนุนคีย์ null หรือค่าหรือ remappingFunction เป็น โมฆะ

คุณสามารถหลีกเลี่ยงการวนรอบโดยใช้forEachวิธีการของรายการของคุณ

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

แต่มันไม่ง่ายไปกว่าวิธีเดิม:

Map<Integer, Boolean> answerMap = new HashMap<>();
for (Answer answer : answerList) {
    answerMap.put(answer.getId(), answer.getAnswer());
}

3
ในกรณีนี้ฉันต้องการใช้ของเก่าสำหรับแต่ละคน ฉันควรพิจารณาข้อผิดพลาดนี้ในการผสานหรือไม่ เนื่องจากการใช้ฟังก์ชันการผสานนี้เป็นรายละเอียดการใช้งานจริง ๆ หรือเป็นเหตุผลที่ดีที่ไม่อนุญาตให้แผนที่เพื่อประมวลผลค่า Null?
Jasper

6
มีการระบุไว้ใน javadoc ของการผสาน แต่ไม่ได้ระบุไว้ในเอกสารของ toMap
Jasper

119
ไม่เคยคิดว่าค่า null ในแผนที่จะส่งผลกระทบกับ API มาตรฐานฉันควรพิจารณาว่าเป็นข้อบกพร่อง
Askar Kalykov

16
ที่จริงเอกสาร API Map.mergeไม่ได้ระบุอะไรเกี่ยวกับการใช้งานของ IMHO นี้เป็นข้อบกพร่องในการใช้งานที่ จำกัด กรณีการใช้งานที่ยอมรับได้อย่างสมบูรณ์แบบซึ่งถูกมองข้าม เมธอดโอเวอร์โหลดของtoMapdo ระบุการใช้Map.mergeแต่ไม่ใช่ OP ที่ใช้
Brett Ryan

11
@ แจสเปอร์ยังมีรายงานข้อผิดพลาดแม้กระทั่งbugs.openjdk.java.net/browse/JDK-8148463
พิกเซล

23

ฉันเขียนCollectorที่ไม่เหมือนกับจาวาเริ่มต้นหนึ่งซึ่งไม่ได้ผิดพลาดเมื่อคุณมีnullค่า:

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;
            });
}

เพียงแค่แทนที่การCollectors.toMap()โทรของคุณเป็นการโทรไปยังฟังก์ชั่นนี้และมันจะแก้ไขปัญหา


1
แต่การอนุญาตให้ใช้nullค่าและการใช้งานputIfAbsentนั้นเล่นได้ไม่ดี มันไม่ได้ตรวจสอบคีย์ที่ซ้ำกันเมื่อพวกเขาแมปไปnull...
โฮล

10

ใช่คำตอบจากฉัน แต่ฉันคิดว่ามันอาจช่วยให้เข้าใจสิ่งที่เกิดขึ้นภายใต้ประทุนในกรณีที่ทุกคนต้องการรหัสอื่น ๆCollector-logic

ฉันพยายามที่จะแก้ปัญหาโดยการเข้ารหัสวิธีการแบบดั้งเดิมและตรงไปตรงมามากขึ้น ฉันคิดว่ามันตรงที่สุด:

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));
      }

    };
  }

}

และการทดสอบโดยใช้ JUnit และ assertj:

  @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");
  }

และคุณจะใช้มันอย่างไร เอาล่ะใช้มันแทนtoMap()แบบทดสอบ ทำให้รหัสโทรดูสะอาดที่สุด

แก้ไข:
ใช้แนวคิดของ Holger ด้านล่างเพิ่มวิธีทดสอบ


1
ผู้ประกอบการไม่ตรวจสอบกุญแจที่ซ้ำกัน หากคุณต้องการหลีกเลี่ยงการตรวจสอบทุกปุ่มคุณสามารถใช้สิ่งที่ต้องการ(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 ใช่นั่นเป็นเรื่องจริง โดยเฉพาะอย่างยิ่งตั้งแต่ที่accumulator()จริงจะตรวจสอบว่า บางทีฉันควรทำลำธารขนานกัน
ซัก

7

นี่คือตัวรวบรวมที่เรียบง่ายกว่าที่เสนอโดย @EmmanuelTouzery ใช้มันหากคุณต้องการ:

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;
            });
}

เราเพิ่งแทนที่nullด้วยวัตถุที่กำหนดเองnoneและดำเนินการย้อนกลับในหมัดเด็ด


5

หากค่าเป็น String แสดงว่าอาจใช้งานได้: map.entrySet().stream().collect(Collectors.toMap(e -> e.getKey(), e -> Optional.ofNullable(e.getValue()).orElse("")))


4
ใช้งานได้เฉพาะในกรณีที่คุณโอเคกับการปรับเปลี่ยนข้อมูล เมธอดดาวน์สตรีมอาจคาดหวังค่า Null แทนที่จะเป็นสตริงว่าง
Sam Buchmiller

3

ให้เป็นไปตาม 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)

เมื่อใดที่เรียกว่า map.merge

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

มันจะทำการnullตรวจสอบเป็นสิ่งแรก

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

ฉันไม่ได้ใช้ Java 8 บ่อยดังนั้นฉันไม่รู้ว่ามีวิธีที่ดีกว่าในการแก้ไขหรือไม่ แต่การแก้ไขมันค่อนข้างยาก

คุณสามารถทำได้:

ใช้ตัวกรองเพื่อกรองค่า NULL ทั้งหมดและในรหัส Javascript ตรวจสอบว่าเซิร์ฟเวอร์ไม่ได้ส่งคำตอบใด ๆ สำหรับรหัสนี้หมายความว่าเขาไม่ได้ตอบกลับ

บางสิ่งเช่นนี้

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

หรือใช้ peek ซึ่งใช้ในการเปลี่ยนองค์ประกอบกระแสสำหรับองค์ประกอบ ใช้ peek คุณสามารถเปลี่ยนคำตอบเป็นสิ่งที่ยอมรับมากขึ้นสำหรับแผนที่ แต่มันหมายถึงการแก้ไขตรรกะของคุณเล็กน้อย

ดูเหมือนว่าถ้าคุณต้องการที่จะออกแบบในปัจจุบันคุณควรหลีกเลี่ยง Collectors.toMap


3

ฉันได้แก้ไขเล็กน้อยการดำเนินงานของเอ็มมานู Touzery

รุ่นนี้;

  • อนุญาตให้ใช้ปุ่ม null
  • อนุญาตให้มีค่า Null
  • ตรวจจับคีย์ที่ซ้ำกัน (แม้ว่าจะเป็นโมฆะ) และส่ง IllegalStateException เหมือนกับในการใช้งาน JDK ดั้งเดิม
  • ตรวจจับคีย์ที่ซ้ำกันเมื่อคีย์ถูกแมปไปยังค่า Null แล้ว กล่าวอีกนัยหนึ่งแยกการแมปด้วยค่า Null จากไม่มีการแมป
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;
        }
    );
}

การทดสอบหน่วย:

@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))
    );
}

1

ขออภัยที่จะเปิดคำถามเก่าอีกครั้ง แต่เนื่องจากมีการแก้ไขเมื่อเร็ว ๆ นี้ที่บอกว่า "ปัญหา" ยังคงอยู่ใน Java 11 ฉันรู้สึกเหมือนฉันต้องการชี้ให้เห็น:

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

ให้ข้อยกเว้นตัวชี้ Null แก่คุณเนื่องจากแผนที่ไม่อนุญาตให้มีค่า Null เป็นค่า สิ่งนี้สมเหตุสมผลเนื่องจากถ้าคุณดูแผนที่สำหรับคีย์kและไม่มีอยู่ค่าที่ส่งคืนจะมีอยู่แล้วnull(ดู javadoc) ดังนั้นถ้าคุณสามารถใส่kค่าnullลงไปแผนที่ก็จะดูเหมือนว่ามันทำงานผิดปกติ

ดังที่มีคนพูดถึงความคิดเห็นมันค่อนข้างง่ายที่จะแก้ปัญหานี้โดยใช้ตัวกรอง:

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

ด้วยวิธีนี้จะไม่มีการnullแทรกค่าลงในแผนที่และยังจะได้รับnullเป็น "ค่า" เมื่อค้นหา id ที่ไม่มีคำตอบในแผนที่

ฉันหวังว่านี่จะสมเหตุสมผลสำหรับทุกคน


1
มันจะสมเหตุสมผลถ้าแผนที่ไม่อนุญาตให้มีค่า Null แต่จะเป็นเช่นนั้น คุณสามารถทำได้answerMap.put(4, null);โดยไม่มีปัญหาใด ๆ คุณถูกต้องด้วยโซลูชันที่เสนอของคุณคุณจะได้รับผลลัพธ์เดียวกันสำหรับ anserMap.get () หากไม่มีอยู่ราวกับว่าค่าจะถูกแทรกเป็นโมฆะ อย่างไรก็ตามหากคุณวนซ้ำทุกรายการของแผนที่มีความแตกต่างอย่างชัดเจน
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());
}

1
upvoting เนื่องจากคอมไพล์นี้ คำตอบที่ยอมรับไม่ได้รวบรวมเนื่องจาก Map :: putAll ไม่มีค่าส่งคืน
Taugenichts

0

เก็บรหัสคำถามทั้งหมดด้วยการปรับแต่งเล็กน้อย

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

ฉันคิดว่านี่เป็นคำตอบที่ดีที่สุด - เป็นคำตอบที่กระชับที่สุดและแก้ไขปัญหา NPE
LConrad

-3

NullPointerException เป็นข้อยกเว้นที่พบบ่อยที่สุด (อย่างน้อยในกรณีของฉัน) เพื่อหลีกเลี่ยงปัญหานี้ฉันไปป้องกันและเพิ่มการตรวจสอบโมฆะและฉันจบลงด้วยการป่องและน่าเกลียดรหัส Java 8 แนะนำทางเลือกเพื่อจัดการการอ้างอิงแบบ null เพื่อให้คุณสามารถกำหนดค่าที่เป็นโมฆะและไม่เป็นโมฆะ

ที่กล่าวว่าฉันจะห่ออ้างอิงที่เป็นโมฆะทั้งหมดในภาชนะเสริม เราไม่ควรทำลายความเข้ากันได้แบบย้อนหลังเช่นกัน นี่คือรหัส

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);
    }
}

1
คำตอบที่ไร้ประโยชน์ทำไมคุณควรกำจัด null เพื่อแก้ไขปัญหานี้ นี่เป็นปัญหาที่Collectors.toMap()ไม่ใช่ค่า Null
Enerccio

@Eccccio สงบลงเพื่อน !! การใช้ค่า Null เป็นวิธีที่ไม่ดี หากคุณใช้ทางเลือกคุณจะไม่พบ NPE ตั้งแต่แรก อ่านเพิ่มเติมเกี่ยวกับประเพณีที่เป็นตัวเลือก
TriCore

1
และทำไมเป็นเช่นนั้น? ค่า Null เป็นเรื่องปกติมันเป็นไลบรารีที่ไม่มีเอกสารที่เป็นปัญหา เป็นตัวเลือกที่ดี แต่ไม่ใช่ทุกที่
Enerccio
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.