当前位置: 首页 > 面试题库 >

Java8 Collections.sort(有时)不对JPA返回的列表进行排序

孔华池
2023-03-14
问题内容

Java8在我的JPA EclipseLink 2.5.2环境中一直在做奇怪的事情。我
昨天不得不删除问题因为在这种情况下,排序受到一种奇怪的JPA行为的影响-我通过强制执行进行最终排序之前先进行排序。

仍然在带有JPA Eclipselink 2.5.2的Java
8中,以下代码有时在我的环境中无法排序(Linux,MacOSX,都使用内部版本1.8.0_25-b17)。它在JDK 1.7环境中按预期工作。

public List<Document> getDocumentsByModificationDate() {
    List<Document> docs=this.getDocuments();
    LOGGER.log(Level.INFO,"sorting "+docs.size()+" by modification date");
    Comparator<Document> comparator=new ByModificationComparator();
    Collections.sort(docs,comparator);
    return docs;
}

当从JUnit测试调用时,上述功能可以正常工作。在生产环境中进行调试时,我得到一个日志条目:

INFORMATION: sorting 34 by modification date

但在TimSort中,nRemaining <2的return语句被命中-因此不进行排序。JPA提供的IndirectList被认为是空的。

static <T> void sort(T[] a, int lo, int hi, Comparator<? super T> c,
                     T[] work, int workBase, int workLen) {
    assert c != null && a != null && lo >= 0 && lo <= hi && hi <= a.length;

    int nRemaining  = hi - lo;
    if (nRemaining < 2)
        return;  // Arrays of size 0 and 1 are always sorted

此解决方法正确排序:

   if (docs instanceof IndirectList) {
        IndirectList iList = (IndirectList)docs;
        Object sortTargetObject = iList.getDelegateObject();
        if (sortTargetObject instanceof List<?>) {
            List<Document> sortTarget=(List<Document>) sortTargetObject;
            Collections.sort(sortTarget,comparator);
        }
    } else {
        Collections.sort(docs,comparator);
    }

题:

这是JPA Eclipselink错误还是我通常可以在自己的代码中对其进行处理?

请注意-我还不能将软件更改为Java8源合规性。当前环境是Java8运行时。

我对这种行为感到惊讶-在生产环境中出现问题时,测试用例正确运行特别令人讨厌。

在https://github.com/WolfgangFahl/JPAJava8Sorting有一个示例项目,
该项目的结构与原始问题相当。

它包含一个带有JUnit测试的http://sscce.org/示例,该示例通过调用em.clear()使问题可重现,从而分离所有对象并强制使用IndirectList。请参阅下面的JUnit案例以供参考。

渴望获取:

// https://stackoverflow.com/questions/8301820/onetomany-relationship-is-not-working
@OneToMany(cascade = CascadeType.ALL, mappedBy = "parentFolder", fetch=FetchType.EAGER)

单位案例有效。如果在JDK 8中使用了FetchType.LAZY或省略了提取类型,则其行为可能与JDK 7中的行为不同(我现在必须对此进行检查)。
为什么呢? 目前,我假设需要指定Eager提取或在要排序的列表上进行一次迭代,基本上是在排序之前手动提取。 还有什么可以做的?

JUnit测试

可以从https://github.com/WolfgangFahl/JPAJava8Sorting中获取persistence.xml和pom.xml。
该测试可以使用MYSQL数据库运行,也可以使用DERBY在内存中运行(默认)

package com.bitplan.java8sorting;

import static org.junit.Assert.assertEquals;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.Persistence;
import javax.persistence.Query;
import javax.persistence.Table;

import org.eclipse.persistence.indirection.IndirectList;
import org.junit.Test;

/**
 * Testcase for 
 * https://stackoverflow.com/questions/26816650/java8-collections-sort-sometimes-does-not-sort-jpa-returned-lists
 * @author wf
 *
 */
public class TestJPASorting {

  // the number of documents we want to sort
  public static final int NUM_DOCUMENTS = 3;

  // Logger for debug outputs
  protected static Logger LOGGER = Logger.getLogger("com.bitplan.java8sorting");

  /**
   * a classic comparator
   * @author wf
   *
   */
  public static class ByNameComparator implements Comparator<Document> {

    // @Override
    public int compare(Document d1, Document d2) {
      LOGGER.log(Level.INFO,"comparing " + d1.getName() + "<=>" + d2.getName());
      return d1.getName().compareTo(d2.getName());
    }
  }

  // Document Entity - the sort target
  @Entity(name = "Document")
  @Table(name = "document")
  @Access(AccessType.FIELD)
  public static class Document {
    @Id
    String name;

    @ManyToOne
    Folder parentFolder;

    /**
     * @return the name
     */
    public String getName() {
      return name;
    }
    /**
     * @param name the name to set
     */
    public void setName(String name) {
      this.name = name;
    }
    /**
     * @return the parentFolder
     */
    public Folder getParentFolder() {
      return parentFolder;
    }
    /**
     * @param parentFolder the parentFolder to set
     */
    public void setParentFolder(Folder parentFolder) {
      this.parentFolder = parentFolder;
    }
  }

  // Folder entity - owning entity for documents to be sorted
  @Entity(name = "Folder")
  @Table(name = "folder")
  @Access(AccessType.FIELD)
  public static class Folder {
    @Id
    String name;

    // https://stackoverflow.com/questions/8301820/onetomany-relationship-is-not-working
    @OneToMany(cascade = CascadeType.ALL, mappedBy = "parentFolder", fetch=FetchType.EAGER)
    List<Document> documents;

    /**
     * @return the name
     */
    public String getName() {
      return name;
    }

    /**
     * @param name the name to set
     */
    public void setName(String name) {
      this.name = name;
    }

    /**
     * @return the documents
     */
    public List<Document> getDocuments() {
      return documents;
    }

    /**
     * @param documents the documents to set
     */
    public void setDocuments(List<Document> documents) {
      this.documents = documents;
    }

    /**
     * get the documents of this folder by name
     * 
     * @return a sorted list of documents
     */
    public List<Document> getDocumentsByName() {
      List<Document> docs = this.getDocuments();
      LOGGER.log(Level.INFO, "sorting " + docs.size() + " documents by name");
      if (docs instanceof IndirectList) {
        LOGGER.log(Level.INFO, "The document list is an IndirectList");
      }
      Comparator<Document> comparator = new ByNameComparator();
      // here is the culprit - do or don't we sort correctly here?
      Collections.sort(docs, comparator);
      return docs;
    }

    /**
     * get a folder example (for testing)
     * @return - a test folder with NUM_DOCUMENTS documents
     */
    public static Folder getFolderExample() {
      Folder folder = new Folder();
      folder.setName("testFolder");
      folder.setDocuments(new ArrayList<Document>());
      for (int i=NUM_DOCUMENTS;i>0;i--) {
        Document document=new Document();
        document.setName("test"+i);
        document.setParentFolder(folder);
        folder.getDocuments().add(document);
      }
      return folder;
    }
  }

  /** possible Database configurations
  using generic persistence.xml:
    <?xml version="1.0" encoding="UTF-8"?>
    <!-- generic persistence.xml which only specifies a persistence unit name -->
    <persistence xmlns="http://java.sun.com/xml/ns/persistence"
      version="2.0">
      <persistence-unit name="com.bitplan.java8sorting" transaction-type="RESOURCE_LOCAL">
        <description>sorting test</description>
        <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
        <exclude-unlisted-classes>false</exclude-unlisted-classes> 
        <properties>
        <!--  set programmatically -->
         </properties>
      </persistence-unit>
    </persistence>
  */
  // in MEMORY database
  public static final JPASettings JPA_DERBY=new JPASettings("Derby","org.apache.derby.jdbc.EmbeddedDriver","jdbc:derby:memory:test-jpa;create=true","APP","APP");
  // MYSQL Database
  //  needs preparation:
  //    create database testsqlstorage;
  //    grant all privileges on testsqlstorage to cm@localhost identified by 'secret';
  public static final JPASettings JPA_MYSQL=new JPASettings("MYSQL","com.mysql.jdbc.Driver","jdbc:mysql://localhost:3306/testsqlstorage","cm","secret");

  /**
   * Wrapper class for JPASettings
   * @author wf
   *
   */
  public static class JPASettings {
    String driver;
    String url;
    String user;
    String password;
    String targetDatabase;

    EntityManager entityManager;
    /**
     * @param driver
     * @param url
     * @param user
     * @param password
     * @param targetDatabase
     */
    public JPASettings(String targetDatabase,String driver, String url, String user, String password) {
      this.driver = driver;
      this.url = url;
      this.user = user;
      this.password = password;
      this.targetDatabase = targetDatabase;
    }

    /**
     * get an entitymanager based on my settings
     * @return the EntityManager
     */
    public EntityManager getEntityManager() {
      if (entityManager == null) {
        Map<String, String> jpaProperties = new HashMap<String, String>();
        jpaProperties.put("eclipselink.ddl-generation.output-mode", "both");
        jpaProperties.put("eclipselink.ddl-generation", "drop-and-create-tables");
        jpaProperties.put("eclipselink.target-database", targetDatabase);
        jpaProperties.put("eclipselink.logging.level", "FINE");

        jpaProperties.put("javax.persistence.jdbc.user", user);
        jpaProperties.put("javax.persistence.jdbc.password", password);
        jpaProperties.put("javax.persistence.jdbc.url",url);
        jpaProperties.put("javax.persistence.jdbc.driver",driver);

        EntityManagerFactory emf = Persistence.createEntityManagerFactory(
            "com.bitplan.java8sorting", jpaProperties);
        entityManager = emf.createEntityManager();
      }
      return entityManager;
    }
  }

  /**
   * persist the given Folder with the given entityManager
   * @param em - the entityManager
   * @param folderJpa - the folder to persist
   */
  public void persist(EntityManager em, Folder folder) {
    em.getTransaction().begin();
    em.persist(folder);
    em.getTransaction().commit();    
  }

  /**
   * check the sorting - assert that the list has the correct size NUM_DOCUMENTS and that documents
   * are sorted by name assuming test# to be the name of the documents
   * @param sortedDocuments - the documents which should be sorted by name
   */
  public void checkSorting(List<Document> sortedDocuments) {
    assertEquals(NUM_DOCUMENTS,sortedDocuments.size());
    for (int i=1;i<=NUM_DOCUMENTS;i++) {
      Document document=sortedDocuments.get(i-1);
      assertEquals("test"+i,document.getName());
    }
  }

  /**
   * this test case shows that the list of documents retrieved will not be sorted if 
   * JDK8 and lazy fetching is used
   */
  @Test
  public void testSorting() {
    // get a folder with a few documents
    Folder folder=Folder.getFolderExample();
    // get an entitymanager JPA_DERBY=inMemory JPA_MYSQL=Mysql disk database
    EntityManager em=JPA_DERBY.getEntityManager();
    // persist the folder
    persist(em,folder);
    // sort list directly created from memory
    checkSorting(folder.getDocumentsByName());

    // detach entities;
    em.clear();
    // get all folders from database
    String sql="select f from Folder f";
    Query query = em.createQuery(sql);
    @SuppressWarnings("unchecked")
    List<Folder> folders = query.getResultList();
    // there should be exactly one
    assertEquals(1,folders.size());
    // get the first folder
    Folder folderJPA=folders.get(0);
    // sort the documents retrieved
    checkSorting(folderJPA.getDocumentsByName());
  }
}

问题答案:

好吧,这是一个完美的教学法,告诉您程序员为什么不应该扩展不是为子类设计的类。诸如“ Effective
Java”之类的书告诉您原因:当超类演化时,尝试拦截每种方法以改变其行为的尝试将失败。

在这里,IndirectList扩展Vector并覆盖几乎所有修改其行为的方法,这是一种清晰的反模式。现在,随着Java 8基类的发展。

从Java
8开始,接口可以具有default方法,因此sort添加了类似的方法,这些方法的优点在于,与Collections.sort实现不同,实现可以覆盖该方法,并提供更适合特定interface实现的实现。Vector这样做有两个原因:现在所有协定都synchronized扩展为排序,并且优化的实现可以将其内部数组传递给该Arrays.sort方法,从而跳过先前实现中已知的复制操作(ArrayList执行相同操作)。

为了立即获得这种好处,即使对于现有代码,Collections.sort也已进行了改进。它委托List.sort默认情况下将委托给另一个方法,该方法实现通过toArray和使用复制的旧行为TimSort。但是,如果List实现重写,List.sort则也会影响行为Collections.sort

                  interface method              using internal
                  List.sort                     array w/o copying
Collections.sort ─────────────────> Vector.sort ─────────────────> Arrays.sort


 类似资料:
  • Java8在我的JPA Eclipselink2.5.2环境中一直在做奇怪的事情。昨天我不得不删除了https://stackoverflow.com/questions/26806183/java-8-sorting-behavious这个问题,因为这个问题的排序受到了一个奇怪的JPA行为的影响--我找到了一个解决方法,在进行最终排序之前强制执行第一个排序步骤。 仍然在Java8和JPA Ecl

  • 问题内容: 我有一个表的以下顺序定义: 如您所见,该表中没有一列。但是,当我尝试插入时,仍尝试以下sql: 我怎样才能禁用它显然具有的功能? 问题答案: 如果您未定义,则默认情况下使用sequelize 。 如果要设置自己的,只需在列上使用即可。

  • 问题内容: 我有一个列表列表(由于必须动态生成它,所以不能是元组),它的结构为一个int和一个float的列表列表,像这样: 我想对它进行排序,但我只能设法获得内置的排序功能,以便按列表的第一个元素对其进行排序,或者什么也不做,但是我需要按列表的第二个元素对它们进行排序,但是我没有不想实现我自己的排序功能。所以我想要的一个例子是: 有人可以告诉我如何获取内置的排序功能之一来执行此操作吗? 问题答案

  • 问题内容: 假设我们在集合中有一些项目,并且我们想使用某些比较器对它们进行排序,并期望结果在列表中: 一种方法是对列表中的项目进行排序,例如: Anothe方法正在使用排序流: 我想知道哪种方法更有效?排序流是否有任何优势(例如在多核上进行Faste排序)? 在运行时复杂性方面/最快方面是高效的。 我不相信自己要实现一个完美的基准,学习并不能真正启发我。 问题答案: 可以肯定地说,两种形式的排序都

  • 我的教授介绍了如何使用ArrayList创建Max Heap类。然后他让我们写一个maxHeapSort方法。我几乎成功地将堆按降序排序,但我假设排序应该按升序。现在我使用一个最大堆为[11,5,8,3,4,1]的ArrayList,它排序为[11,8,5,3,4,1]。 这是我的maxHeapSort代码: 下面是我的教授给出的heapifyDown方法: 这是我的测试代码:

  • 问题内容: 我在Python中有两个列表 我想对第一个列表进行排序,并使用结果对第二个列表进行排序。 换句话说,结果应为: 我知道如何分别对每个列表进行排序,但是如何使用对另一个列表进行排序所产生的索引排列来对一个列表进行排列呢? 问题答案: 施瓦兹变换