简体   繁体   English

HashCode 抛出空指针异常

[英]HashCode throws a nullpointer exception

I have a puzzle for you.我有一个谜题给你。

I am making a herb store web app and this is my database:我正在制作一个药草商店 web 应用程序,这是我的数据库:

在此处输入图像描述

  • The store can have many products商店可以有很多产品
  • A product can contain many herbs一个产品可以包含许多草药

These are my JPA classes:这些是我的 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;
    }
...
}

Adding a new product with a list of herbs works fine.添加带有草药列表的新产品效果很好。 But when i run this and try to get the products in the store, i get a NullPointerException:但是当我运行它并尝试在商店中获取产品时,我得到一个 NullPointerException:

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

The hashCode function of ContainsJPA throws this exception when getting the id for the product. ContainsJPA 的 hashCode function 在获取产品的 id 时会抛出此异常。 Why is it doing that, considering that the 'contains' table in the DB has this id?考虑到数据库中的“包含”表具有此 ID,为什么要这样做? I can't figure out why this is happening.我无法弄清楚为什么会这样。 Please help.请帮忙。

Your hashCode and equals implementations are incorrect.您的 hashCode 和 equals 实现不正确。

The problems with it, in a nutshell:简而言之,它的问题是:

  • They do not adhere to the 'delegation' style (they do not delegate the job of determining equality to the relevant classes)他们不遵守“委托”风格(他们不将确定平等的工作委托给相关类)
  • They do not answer the central question of what the object represents: Row in a DB, or the notion that the row in the DB is trying to represent.他们没有回答 object 代表什么的核心问题:数据库中的行,或者数据库中的行试图代表的概念。

Delegate equality checks委托平等检查

Both hashCode and equals are specced to require that you do not throw NPEs out of them. hashCode 和 equals 都要求您不要将 NPE 扔出。 For equals, that means you can't just call, say, a.equals(b) - you'd have to make that a == null? b == null: a.equals(b)对于equals,这意味着你不能只调用a.equals(b) - 你必须让a == null? b == null: a.equals(b) a == null? b == null: a.equals(b) (and because this 'never throw' is transitive, a.equals(b) is fine, even if b is null), or use the helper Objects.equal(a, b) instead. a == null? b == null: a.equals(b) (并且因为这个“从不抛出”是传递性的, a.equals(b)很好,即使 b 为空),或者使用帮助器Objects.equal(a, b)反而。

For hashcode, it means that null values must be defined as having some predefined value for the sake of hashing.对于哈希码,这意味着必须将 null 值定义为具有一些预定义值以便进行哈希处理。 Also, more generally, whenever you have a 'sub object' (eg a field of some non-primitive type', the general idea is for hashCode and equals to cascade: Use productJPA.hashCode() and not productJPA.getId() .此外,更一般地说,每当您有一个“子对象”(例如,一些非原始类型的字段)时,一般的想法是 hashCode 并且等于级联:使用productJPA.hashCode()而不是productJPA.getId()

Same goes for equals.平等也一样。 Don't do this:不要这样做:

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

but do this:但这样做:

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

And if 2 herb JPAs are to be considered equal if their IDs are equal, then the HerbJPA class's equals() method should be defined accordingly, and if not, then not.如果 2 个草本 JPA 的 ID 相等,则认为它们相等,则应相应定义 HerbJPA 类的equals()方法,如果不相等,则不定义。 It is not the job of your ContainsJPA class to know how to calculate if 2 herbJPA instances are equal - herbJPA can do that, itself.知道如何计算 2 个herbJPA 实例是否相等不是您的 ContainsJPA class 的工作——herbJPA 本身可以做到这一点。 In passing you avoid a ton of null issues by doing it this way.通过这种方式,您可以避免大量 null 问题。

Note, you can let lombok take care of all this boilerplate for you.请注意,您可以让lombok为您处理所有这些样板文件。

Next, we get to some hairy issues with JPA and equality in particular.接下来,我们会遇到一些关于 JPA 的棘手问题,尤其是平等问题。

The common strategy to do equals/hashCode in the java ecosystem (outside of JPA/Hibernate) is to look at all fields that are part of an object's identity, which is usually all of them.在 java 生态系统(JPA/Hibernate 之外)中执行 equals/hashCode 的常用策略是查看作为对象身份一部分的所有字段,通常是所有字段。 The problem is, that doesn't work well with JPA: Most of the getter methods on a JPA object are proxies which cause DB queries if you invoke them.问题是,这不适用于 JPA:JPA object 上的大多数 getter 方法都是代理,如果您调用它们会导致数据库查询。 With a sufficiently interconnected db structure (lots of references), that means a single equals call ends up querying half your DB, takes a ton of memory, and half an hour to complete, obviously not a feasible solution.使用充分互连的数据库结构(大量引用),这意味着单个equals调用最终会查询一半的数据库,需要大量的 memory 和半小时才能完成,显然不是一个可行的解决方案。

The key question is: What does your object actually represent, and as far as I know, JPA does not give clear guidance.关键问题是:你的object究竟代表什么,据我所知,JPA并没有给出明确的指导。

An instance of HerbsJPA represents a row in a database HerbsJPA 的一个实例代表数据库中的一行

Then we can draw the following conclusions:那么我们可以得出以下结论:

  • As always, by spec, an object is always equal to itself: if (this == other) return true;与往常一样,按照规范,object 始终等于自身: if (this == other) return true; . . Otherwise...否则...
  • If either or both of the objects have no set unid, then they cannot be equal to each other - 2 unwritten rows, even if entirely identical for every field in the object, still does not represent 'the same row', therefore, not equal!如果其中一个或两个对象没有设置 unid,则它们不能彼此相等- 2 个未写入的行,即使 object 中的每个字段完全相同,仍然不代表“同一行”,因此,不等于!
  • If both objects have a set unid, then they are equal if the unids are, and otherwise, they are not.如果两个对象都有一个设置的 unid,那么如果 unid 相同,则它们相等,否则它们不相等。 Regardless of all the other values!不管所有其他值! - 2 different rows with identical values are... still two different rows. - 具有相同值的 2 个不同行是......仍然是两个不同的行。

This view incidentally is also convenient in that you entirely avoid that 'whoops it queries the entire DB' issue.顺便说一下,这个视图也很方便,因为您完全避免了“哎呀它查询整个数据库”的问题。 unids are not expensive to fetch, and are usually prefetched already. unid 的获取成本并不高,而且通常已经预取。

An instance of HerbsJPA represents a 'herb'. HerbsJPA 的一个实例代表一个“草本”。

If this is the case, may I suggest your class is misnamed?如果是这种情况,我可以建议您的 class 名称错误吗? It should be 'Herb', probably.应该是“草本”吧。 Maybe 'HerbJpa' (NB: JPA in all-caps is a violation of the most common style rule).也许“HerbJpa”(注意:全大写的 JPA 违反了最常见的样式规则)。

Then the most sensical solution is to AVOID checking the unid entirely, and look only at all the other fields (or at least, all the other ones that represent something about the herb's identity. This is usually most of them, but sometimes you can get away with defining some property that would cause a storm of DB queries, such as 'a list of associated herbs', represented in the DB with a join table, as 'not part of the identity'. After all, 'the unid in the db' is an incidental implementation detail of the notion of a 'herb' and therefore couldn't possibly be part of the identity of it!然后最明智的解决方案是避免完全检查unid,而只查看所有其他字段(或者至少,所有其他代表草药身份的字段。这通常是其中的大多数,但有时你可以得到不再定义一些会导致数据库查询风暴的属性,例如“关联草药列表”,在数据库中用连接表表示,作为“不是身份的一部分”。毕竟,“ db' 是“草本”概念的附带实现细节,因此不可能成为它的身份的一部分!

The downside of this view is of course that 'storm of DB calls' issue.这种观点的缺点当然是“数据库调用风暴”问题。

Generally I advise you treat these objects as representing 'row in a table' and not 'the actual herb', in which case, your equals and hashCode methods become relatively simple, and the name of the class is fine (well, it should be 'Jpa', not 'JPA', but other than that).一般来说,我建议您将这些对象视为代表“表格中的行”而不是“实际的药草”,在这种情况下,您的 equals 和 hashCode 方法变得相对简单,并且 class 的名称很好(嗯,应该是'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);
}

Not 100% sure, but doesn't AbstractRowReader first load the collection and then "hydrate" the associated entities?不是 100% 肯定,但 AbstractRowReader 不是首先加载集合然后“水合”关联实体吗?

AbstractRowReader#finishUp() AbstractRowReader#finishUp()

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

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

Which means when creating the collection, the product_id is known, but the ProductJPA instance hasn't been hydrated yet.这意味着在创建集合时,product_id 是已知的,但 ProductJPA 实例尚未水合。

tbh, I think it's not great practice to derive a hashcode from associated entities. tbh,我认为从关联实体派生哈希码不是很好的做法。 I'd probably do something like我可能会做类似的事情

public class ContainsJPA {
  @Id
  private Long id;

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

to get some distribution (the '1000' is a magic number, depending on what typical collection sizes are).获得一些分布(“1000”是一个神奇的数字,取决于典型的集合大小)。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM