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

Java8在我的JPA EclipseLink 2.5.2环境中继续做一些奇怪的事情。 我不得不删除问题https://stackoverflow.com/questions/26806183/java-8-sorting-behaviour昨天,因为在这种情况下的排序受到奇怪的JPA行为的影响 – 我通过强制执行该方法找到了解决方法在进行最终排序之前的第一个排序步骤。

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

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

从JUnit测试调用时,上述函数正常工作。 在生产环境中进行debbuging时,我会得到一个日志条目:

 INFORMATION: sorting 34 by modification date 

但在TimSort中,nRemaining <2的返回语句被命中 – 因此不会发生排序。 JPA提供的IndirectList(请参阅jpa返回的集合? )被认为是空的。

 static  void sort(T[] a, int lo, int hi, Comparator 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 sortTarget=(List) 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) 

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

JUnit测试

persistence.xml和pom.xml可以从https://github.com/WolfgangFahl/JPAJava8Sorting获取测试可以使用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 { // @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 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 getDocuments() { return documents; } /** * @param documents the documents to set */ public void setDocuments(List documents) { this.documents = documents; } /** * get the documents of this folder by name * * @return a sorted list of documents */ public List getDocumentsByName() { List 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 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()); 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:     sorting test org.eclipse.persistence.jpa.PersistenceProvider false      */ // 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 jpaProperties = new HashMap(); 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 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 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 

等待修复错误https://bugs.eclipse.org/bugs/show_bug.cgi?id=446236 。 在可用时或快照时使用以下依赖项。

  org.eclipse.persistence eclipselink 2.6.0  

在此之前使用问题的解决方法:

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

或尽可能指定急切提取:

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

你遇到的问题不是排序。

TimSort通过Arrays.sort调用,它执行以下操作:

 TimSort.sort(a, 0, a.length, c, null, 0, 0); 

所以你可以看到TimSort得到的数组的大小是0或1。

Collections.sort调用Arrays.sort ,它执行以下操作。

 Object[] a = list.toArray(); Arrays.sort(a, (Comparator)c); 

因此,您的集合未被排序的原因是它返回一个空数组。 因此,通过返回空数组,正在使用的集合不符合集合API。

你说你有一个持久层。 所以听起来问题是你正在使用的库以一种懒惰的方式检索实体,除非必须,否则不会填充其后备数组。 仔细查看您要尝试排序的集合,看看它是如何工作的。 您的原始unit testing没有显示任何内容,因为它没有尝试对生产中使用的相同集合进行排序。