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 抛出空指针异常的主要内容,如果未能解决你的问题,请参考以下文章