HashCode 抛出空指针异常

Posted

技术标签:

【中文标题】HashCode 抛出空指针异常【英文标题】:HashCode throws a nullpointer exception 【发布时间】:2020-11-22 02:03:32 【问题描述】:

我有一个谜题要给你。

我正在制作一个药草商店网络应用程序,这是我的数据库:

商店可以有很多产品 一个产品可以包含许多草药

这些是我的 JPA 课程:

public class StoreJPA 
...
    @OneToMany(mappedBy="storeJpa", cascade = CascadeType.ALL, orphanRemoval=true, fetch=FetchType.EAGER)
    private Set<ProductJPA> specialOffers = new HashSet<ProductJPA>();
...


public class ProductJPA 
    @ManyToOne
    @JoinColumn(name="store_id")
    private StoreJPA storeJpa;

    @OneToMany(mappedBy="productJpa", cascade = CascadeType.ALL, orphanRemoval=true, fetch=FetchType.EAGER)
    private Set<ContainsJPA> contains = new HashSet<ContainsJPA>();
...
    private Set<HerbJPA> getHerbs()
        return contains.stream().map(h -> h.getHerbJpa()).collect(Collectors.toSet());
    

    @Override
    public int hashCode()
        long h = 1125899906842597L; // prime
        
        for(ProductHasHerbJPA phh : contains)
            h = 31*h + phh.getHerbJpa().getId();
        
        
        return (int)(31*h + storeJpa.getId());
    
    
    @Override
    public boolean equals(Object o)
        if(o!=null && o instanceof ProductJPA)
            if(o==this)
                return true;
            return ((ProductJPA)o).getStoreJpa().getId()==storeJpa.getId() && 
                    ((ProductJPA)o).getHerbs().equals(getHerbs()) // compare herbs they contain
        
        return false;
    
...


public class ContainsJPA 
    @Id
    private Long id;

    @ManyToOne
    @JoinColumn(name="product_id")
    private ProductJPA productJpa;
    
    @ManyToOne
    @JoinColumn(name="herb_id")
    private HerbJPA herbJpa;

...
    @Override
    public int hashCode()
        long h = 1125899906842597L + productJpa.getId();    // <-- nullpointer exception    
        
        return (int)(31*h + herbJpa.getId());
    
    
    @Override
    public boolean equals(Object o)
        if( o != null && o instanceof HerbLocaleJPA) 
            if(o==this) 
                return true;
            
            return ((ProductHasHerbJPA)o).getHerbJpa().getId()==herbJpa.getId() && 
                    ((ProductHasHerbJPA)o).getProductJpa().getId()==productJpa.getId();
        
        
        return false;
    
...

添加带有草药列表的新产品效果很好。 但是当我运行它并尝试在商店中获取产品时,我得到一个 NullPointerException :

java.lang.NullPointerException 在 com.green.store.entities.ContainsJPA.hashCode(ContainsJPA.java:64) 在 java.util.HashMap.hash(HashMap.java:339) 在 java.util.HashMap.put(HashMap.java:612) 在 java.util.HashSet.add(HashSet.java:220) 在 java.util.AbstractCollection.addAll(AbstractCollection.java:344) 在 org.hibernate.collection.internal.PersistentSet.endRead(PersistentSet.java:327) 在 org.hibernate.engine.loading.internal.CollectionLoadContext.endLoadingCollection(CollectionLoadContext.java:234) 在 org.hibernate.engine.loading.internal.CollectionLoadContext.endLoadingCollections(CollectionLoadContext.java:221) 在 org.hibernate.engine.loading.internal.CollectionLoadContext.endLoadingCollections(CollectionLoadContext.java:194) 在 org.hibernate.loader.plan.exec.process.internal.CollectionReferenceInitializerImpl.endLoading(CollectionReferenceInitializerImpl.java:154) 在 org.hibernate.loader.plan.exec.process.internal.AbstractRowReader.finishLoadingCollections(AbstractRowReader.java:249) 在 org.hibernate.loader.plan.exec.process.internal.AbstractRowReader.finishUp(AbstractRowReader.java:212) 在 org.hibernate.loader.plan.exec.process.internal.ResultSetProcessorImpl.extractResults(ResultSetProcessorImpl.java:133) 在 org.hibernate.loader.plan.exec.internal.AbstractLoadPlanBasedLoader.executeLoad(AbstractLoadPlanBasedLoader.java:122) 在 org.hibernate.loader.plan.exec.internal.AbstractLoadPlanBasedLoader.executeLoad(AbstractLoadPlanBasedLoader.java:86) 在 org.hibernate.loader.entity.plan.AbstractLoadPlanBasedEntityLoader.load(AbstractLoadPlanBasedEntityLoader.java:167) 在 org.hibernate.persister.entity.AbstractEntityPersister.load(AbstractEntityPersister.java:4087) 在 org.hibernate.event.internal.DefaultLoadEventListener.loadFromDatasource(DefaultLoadEventListener.java:508) 在 org.hibernate.event.internal.DefaultLoadEventListener.doLoad(DefaultLoadEventListener.java:478) 在 org.hibernate.event.internal.DefaultLoadEventListener.load(DefaultLoadEventListener.java:219) 在 org.hibernate.event.internal.DefaultLoadEventListener.proxyOrLoad(DefaultLoadEventListener.java:278) 在 org.hibernate.event.internal.DefaultLoadEventListener.doOnLoad(DefaultLoadEventListener.java:121) 在 org.hibernate.event.internal.DefaultLoadEventListener.onLoad(DefaultLoadEventListener.java:89) 在 org.hibernate.internal.SessionImpl.fireLoad(SessionImpl.java:1239) 在 org.hibernate.internal.SessionImpl.internalLoad(SessionImpl.java:1122) 在 org.hibernate.type.EntityType.resolveIdentifier(EntityType.java:672) 在 org.hibernate.type.EntityType.resolve(EntityType.java:457) 在 org.hibernate.engine.internal.TwoPhaseLoad.doInitializeEntity(TwoPhaseLoad.java:165) 在 org.hibernate.engine.internal.TwoPhaseLoad.initializeEntity(TwoPhaseLoad.java:125) 在 org.hibernate.loader.plan.exec.process.internal.AbstractRowReader.performTwoPhaseLoad(AbstractRowReader.java:238) 在 org.hibernate.loader.plan.exec.process.internal.AbstractRowReader.finishUp(AbstractRowReader.java:209) 在 org.hibernate.loader.plan.exec.process.internal.ResultSetProcessorImpl.extractResults(ResultSetProcessorImpl.java:133) 在 org.hibernate.loader.plan.exec.internal.AbstractLoadPlanBasedLoader.executeLoad(AbstractLoadPlanBasedLoader.java:122) 在 org.hibernate.loader.plan.exec.internal.AbstractLoadPlanBasedLoader.executeLoad(AbstractLoadPlanBasedLoader.java:86) 在 org.hibernate.loader.entity.plan.AbstractLoadPlanBasedEntityLoader.load(AbstractLoadPlanBasedEntityLoader.java:167) 在 org.hibernate.persister.entity.AbstractEntityPersister.load(AbstractEntityPersister.java:4087) 在 org.hibernate.event.internal.DefaultLoadEventListener.loadFromDatasource(DefaultLoadEventListener.java:508) 在 org.hibernate.event.internal.DefaultLoadEventListener.doLoad(DefaultLoadEventListener.java:478) 在 org.hibernate.event.internal.DefaultLoadEventListener.load(DefaultLoadEventListener.java:219) 在 org.hibernate.event.internal.DefaultLoadEventListener.doOnLoad(DefaultLoadEventListener.java:116) 在 org.hibernate.event.internal.DefaultLoadEventListener.onLoad(DefaultLoadEventListener.java:89) 在 org.hibernate.internal.SessionImpl.fireLoad(SessionImpl.java:1239) 在 org.hibernate.internal.SessionImpl.immediateLoad(SessionImpl.java:1097) ...

ContainsJPA 的 hashCode 函数在获取产品的 id 时会抛出此异常。考虑到数据库中的“包含”表具有此 ID,为什么要这样做? 我无法弄清楚为什么会这样。请帮忙。

【问题讨论】:

【参考方案1】:

您的 hashCode 和 equals 实现不正确。

简而言之,它的问题:

他们不遵守“委托”风格(他们不将确定平等的工作委托给相关类) 他们没有回答对象代表什么的核心问题:数据库中的行,或者数据库中的行试图表示的概念。

委托相等性检查

hashCode 和 equals 都被指定为 要求你不要把 NPE 扔出去。对于equals,这意味着你不能只调用a.equals(b) - 你必须调用a == null ? b == null : a.equals(b) (并且因为这个'never throw' 是传递的,a.equals(b) 很好,即使 b 为空),或者改用助手Objects.equal(a, b)

对于哈希码,这意味着必须将空值定义为具有一些预定义的值以进行哈希处理。此外,更一般地,每当您有一个“子对象”(例如,某个非原始类型的字段)时,一般的想法是使用 hashCode 并等于级联:使用 productJPA.hashCode() 而不是 productJPA.getId()

平等也是如此。不要这样做:

(ProductHasHerbJPA)o).getHerbJpa().getId()==herbJpa.getId()

但是这样做:

Objects.equals(o.getHerbJpa(), herbJpa);

如果 2 个草本 JPA 的 ID 相等,则认为它们相等,则应相应定义 HerbJPA 类的 equals() 方法,如果不相等,则不定义。知道如何计算 2 个herbJPA 实例是否相等不是您的 ContainsJPA 类的工作——herbJPA 本身可以做到这一点。通过这种方式,您可以避免大量的 null 问题。

注意,您可以让lombok 为您处理所有这些样板文件。

接下来,我们将讨论 JPA 和平等方面的一些棘手问题。

在 Java 生态系统(JPA/Hibernate 之外)中执行 equals/hashCode 的常见策略是查看作为对象身份一部分的所有字段,通常是所有这些。问题是,这不适用于 JPA:JPA 对象上的大多数 getter 方法都是代理,如果您调用它们会导致数据库查询。使用充分互连的数据库结构(大量引用),这意味着单个 equals 调用最终会查询一半的数据库,需要大量内存,并且需要半小时才能完成,显然不是一个可行的解决方案。

关键问题是:你的对象实际上代表什么,据我所知,JPA 并没有给出明确的指导。

HerbsJPA 的一个实例代表数据库中的一行

那么我们可以得出以下结论:

与往常一样,按照规范,对象始终等于自身:if (this == other) return true;。否则... 如果其中一个或两个对象没有设置 unid,则 它们不能彼此相等 - 2 未写行,即使对象中的每个字段完全相同,仍然不代表因此,“同一行”不相等! 如果两个对象都有一个设置的 unid,那么如果 unid 相同,则它们相等,否则它们不相等。 无论所有其他值如何! - 具有相同值的 2 行不同的行......仍然是两个不同的行。

顺便说一句,此视图也很方便,因为您可以完全避免“哎呀它查询整个数据库”的问题。获取 unid 并不昂贵,而且通常已经预取。

HerbsJPA 的一个实例代表一种“药草”。

如果是这种情况,我可以建议您的班级名称错误吗?应该是“草本”吧。也许是“HerbJpa”(注意:全大写的 JPA 违反了最常见的样式规则)。

那么最明智的解决方案是避免完全检查unid,并且只查看所有其他字段(或者至少,所有其他代表草药身份的字段。这是通常它们中的大多数,但有时您可以定义一些会导致数据库查询风暴的属性,例如“关联草药列表”,在数据库中用连接表表示,作为“不是身份的一部分” '. 毕竟,'the unid in the db' 是'herb' 概念的附带实现细节,因此不可能是它的身份的一部分!

这种观点的缺点当然是“数据库调用风暴”问题。

一般来说,我建议您将这些对象视为表示“表格中的行”而不是“实际的药草”,在这种情况下,您的 equals 和 hashCode 方法变得相对简单,并且类的名称很好(嗯,它应该是“Jpa”,而不是“JPA”,但除此之外)。

@Override public int hashCode() 
    return id == null ? super.hashCode() : (int) id;
    // note, other answer's id %1000 is silly;
    // it is needlessly inefficient, don't do it that way.


@Override public boolean equals(Object other) 
    if (other == this) return true;
    if (other == null || other.getClass() != ContainsJPA.class) return false;
    return id == null ? false : id.equals(other.id);

【讨论】:

我想根据它们所在的商店以及它们所含的草药来区分产品。 我想我会删除“包含”表并添加一个“包含”列【参考方案2】:

不是 100% 确定,但 AbstractRowReader 不是首先加载集合并然后“水合”关联实体吗?

AbstractRowReader#finishUp()

  ...
  // now we can finalize loading collections
  finishLoadingCollections( context );

  // finally, perform post-load operations
  postLoad( postLoadEvent, context, hydratedEntityRegistrations, afterLoadActionList 
);

这意味着在创建集合时,product_id 是已知的,但 ProductJPA 实例尚未水合。

tbh,我认为从关联实体派生哈希码不是很好的做法。我可能会做类似的事情

public class ContainsJPA 
  @Id
  private Long id;

  @Override
  public int hashCode()
    return id == null ? super.hashCode() : id % 1000;
  

获得一些分布(“1000”是一个神奇的数字,取决于典型的集合大小)。

【讨论】:

@rzwitserloot,这是由于哈希映射分布。具有相同hashCode 的所有项目都放在同一个“桶”中。通过返回 (int)id,您的代码可以工作,但每个“桶”只能容纳 1 个项目,在某种程度上使 hashCode 的点无效。 那是.. 不是 hashmap 的工作方式。每个哈希码有 1 个项目是最佳的;存储桶包含一系列哈希码。如果您不遗余力地让不同的对象返回相同的哈希码,那么您正在编写非常低效的哈希算法! @rzwitserloot,我们不要用关于哈希算法的争论来拖钓这篇文章。 Lazaruss 需要修复他的 NPE,我认为我们的两个答案都应该能帮助他解决这个问题。

以上是关于HashCode 抛出空指针异常的主要内容,如果未能解决你的问题,请参考以下文章

为啥非空列表会抛出空指针异常?

Android Canvas drawcolor 抛出空指针异常

Play 2.4 Finder 抛出空指针异常

SAX XML 解析器抛出空指针异常

JOOQ“IN”查询抛出空指针异常

JSONObject 创建抛出空指针异常