Java: TreeMap 是否有一种很好的方法来搜索重复字符串作为复合键的一部分

Java: TreeMap is there a nice way to search for duplicate strings as part of a composite key

我有一个 TreeMap,其中键是基于两个字段的复合键。我希望能够在 TreeMap 中搜索第二个关键元素的匹配项 - 该元素可能有重复项。为了解释我正在尝试做的事情,请参阅以下内容,

public class CountySchoolsController {

    static TreeMap<StudentKey, Student> schoolsMap = new TreeMap<>();

    public static void main(String args[]) {
        createSchoolsTreeMap();
        System.out.println(schoolsMap.get(new StudentKey(1, "Holmes")));
    }

    private static TreeMap<StudentKey, Student> createSchoolsTreeMap() {
        Student s1 = new Student(1, "Sherlock", "Holmes");
        Student s2 = new Student(2, "John", "Watson");
        Student s3 = new Student(3, "Franklin", "Holmes");
        schoolsMap.put(new StudentKey(s1.getSchoolId(), s1.getLastname()), s1);
        schoolsMap.put(new StudentKey(s2.getSchoolId(), s2.getLastname()), s2);
        schoolsMap.put(new StudentKey(s3.getSchoolId(), s3.getLastname()), s3);
        return schoolsMap;
    }
}

public class StudentKey implements Comparable<StudentKey>{

    int schoolId;
    String lastname;

    public StudentKey(int id, String lastname){
        this.schoolId = id;
        this.lastname = lastname;
    }

    public int getSchoolId() {
        return schoolId;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        StudentKey that = (StudentKey) o;
        return schoolId == that.schoolId &&
                Objects.equals(lastname, that.lastname);
    }

    @Override
    public int hashCode() {
        return Objects.hash(schoolId, lastname);
    }

    @Override
    public int compareTo(StudentKey o) {
        return (this.schoolId + this.lastname).compareTo(o.schoolId + o.lastname);
    }
}

public class Student {

    int schoolId;
    String firstname;
    String lastname;

    public Student(int schoolId, String firstname, String lastname) {
        this.schoolId = schoolId;
        this.firstname = firstname;
        this.lastname = lastname;
    }

    public int getSchoolId() {
        return schoolId;
    }

    public String getFirstname() {
        return firstname;
    }

    public String getLastname() {
        return lastname;
    }

    @Override
    public String toString() {
        return "Student{" +
                "schoolId=" + schoolId +
                ", firstname='" + firstname + '\'' +
                ", lastname='" + lastname + '\'' +
                '}';
    }
}

当我 运行 这个代码时它 运行 很好并打印找到的 Student,

Student{schoolId=1, firstname='Sherlock', lastname='Holmes'}

然而,我希望能够做的是仅搜索 lastname 中的 Holmes 和 return 两个由 ID 1 和 3 表示的记录。以及进行搜索喜欢这个我还需要能够对键的精确匹配进行搜索(如上例所示)。

理想情况下,如果我可以将 wildcard 用于 schoolId 就好了,但我认为这是不可能的。

我可以 return 键集值并对其进行迭代以仅在 lastname 上找到匹配项,但我认为这不会非常 performant - 如果您不同意,请告诉我,或者这是否是实现它的最佳方式?或者我应该以另一种方式实施吗?

试试这个。它流式传输映射的 entrySet,仅过滤姓氏,然后映射到与该名称关联的值并将其放入列表中。我必须为此创建 lastname 字段 public。在字段中输入 getters 会很有用。


        List<Student> list = schoolsMap.entrySet().stream()
                .filter(e -> e.getKey().lastname
                        .equals("Holmes"))
                .map(Entry::getValue)
                .collect(Collectors.toList());


        list.forEach(System.out::println);

我决定在这方面更进一步。如果你在你的 StudentKey class 中为所有 key 属性设置 getters,你可以执行以下操作:

获取 Holmes

的所有姓氏
      List<Student> names = getStudentsForKeyAttribute(
                StudentKey::getLastName, "Holmes");

      names.forEach(System.out::println);

获取 id = 3

的学生

      List<Student> ids = getStudentsForKeyAttribute(StudentKey::getSchoolId, 3);

      ids.forEach(System.out.println); 

以下方法接受 AttributeExtractor 函数以从 StudentKey 中提取适当的属性,然后根据提供的参数进行过滤。

        public <T> List<Student> getStudentsForKeyAttribute(
            Function<StudentKey, T> attrExtractor, T keyAttribute) {
            return schoolsMap.entrySet().stream()
                .filter(e -> attrExtractor.apply(e.getKey())
                        .equals(keyAttribute))
                .map(Entry::getValue)
                .collect(Collectors.toList());
        }

主要编辑:我添加了数据保留功能。第一次通过每个属性时,它会为该属性和 returns 请求的值构建映射。未来的调用使用现有地图。

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
import java.util.function.Function;


// Main attribute for lookup
enum Attribute {
    LASTNAME, GENDER
}

// subattribute for gender.  For lastname it would jut be a name.
// I decided to use an enum for gender.
enum Gender {
    M, F, O
};

public class CompositeSearch {
    // Map of map.
    // Example:
    //   Outer Map is for LastName attribute
    //   Inner map as all lists for common last names.  All names are included.
    //     I had to use an Object to allow for different types (enums, strings, ints)
    Map<Attribute, Map<Object, List<Student>>> studentsByAttribute = new HashMap<>();


    // this provides an extractor for each type requested. It just maps the 
    // Attribute to the Student Key method call for that type.
    Map<Attribute, Function<StudentKey, ?>> extractorsForType = new HashMap<>() {
        {
            put(Attribute.LASTNAME,
                    StudentKey::getLastName);
            put(Attribute.GENDER, StudentKey::getGender);
        }
    };

    // intiial data base
    TreeMap<StudentKey, Student> schoolsMap = new TreeMap<>();

    public static void main(String args[]) {
        new CompositeSearch().start();
    }

    public void start() {
        createSchoolsTreeMap();

        // getting all female students.
        List<Student> list = getStudentsForKeyAttribute(
                Attribute.GENDER, Gender.F);

        list.forEach(System.out::println);
        System.out.println();

        // getting all students with last name of holmes.
        list = getStudentsForKeyAttribute(Attribute.LASTNAME, "Holmes");
        list.forEach(System.out::println);

        // All maps for Gender and lastnames have been created so 
        // the lookups below require two map retrievals.  The attribute and the 
        // sub attribute
        System.out.println();
        list = getStudentsForKeyAttribute(
                Attribute.GENDER, Gender.M);

        list.forEach(System.out::println);
        System.out.println();

        list = getStudentsForKeyAttribute(Attribute.LASTNAME, "Watson");
        list.forEach(System.out::println);


    }

    public <T> List<Student> getStudentsForKeyAttribute(
            Attribute attr, T keyAttribute) {

        @SuppressWarnings("unchecked")
        Function<StudentKey, T> extractor = (Function<StudentKey, T>) extractorsForType
                .get(attr);

        if (!studentsByAttribute.containsKey(attr)) {
            // need to create the map.
            System.out.println("Building map for all " + attr);
            // sub attribute map
            Map<Object, List<Student>> subMap = new HashMap<>();

            studentsByAttribute.put(attr, subMap);

            for (Map.Entry<StudentKey, ?> e : schoolsMap
                    .entrySet()) {
              T subAttribute = extractor.apply(e.getKey());

                            subMap.compute(subAttribute,
                                    (k, v) -> v == null
                                            ?  new ArrayList<>()
                                            : v)
                            .add((Student)e.getValue());

            }
        } else {
            System.out.println("Using existing map for all " + attr);
        }
        return studentsByAttribute.get(attr).get(keyAttribute);

    }

    // from here on out, everything is pretty normal.
    private TreeMap<StudentKey, Student> createSchoolsTreeMap() {
        List<Student> students = List.of(
                new Student(1, "Sherlock", "Holmes",
                        Gender.M),
                new Student(2, "John", "Watson", Gender.M),
                new Student(3, "Franklin", "Holmes",
                        Gender.M),
                new Student(4, "Frances", "Holmes",
                        Gender.F),
                new Student(5, "Mary", "Wilson", Gender.F),
                new Student(6, "Martha", "Watson",
                        Gender.F));
        for (Student s : students) {
            schoolsMap.put(new StudentKey(s), s);
        }

        return schoolsMap;
    }

}

class StudentKey implements Comparable<StudentKey> {

    private int schoolId;
    private String lastname;
    private Gender gender;

    public StudentKey(Student student) {
        this.schoolId = student.getSchoolId();
        this.lastname = student.getLastname();
        this.gender = student.getGender();
    }

    public int getSchoolId() {
        return schoolId;
    }

    public String getLastName() {
        return lastname;
    }

    public Gender getGender() {
        return gender;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o)
            return true;
        if (o == null || getClass() != o.getClass())
            return false;
        StudentKey that = (StudentKey) o;
        return schoolId == that.schoolId
                && Objects.equals(lastname, that.lastname);
    }

    @Override
    public int hashCode() {
        return Objects.hash(schoolId, lastname);
    }

    @Override
    public int compareTo(StudentKey o) {
        return (this.schoolId + this.lastname)
                .compareTo(o.schoolId + o.lastname);
    }

}

class Student {

    int schoolId;
    String firstname;
    String lastname;
    Gender gender;

    public Student(int schoolId, String firstname,
            String lastname, Gender gender) {
        this.schoolId = schoolId;
        this.firstname = firstname;
        this.lastname = lastname;
        this.gender = gender;
    }

    public int getSchoolId() {
        return schoolId;
    }

    public String getFirstname() {
        return firstname;
    }

    public String getLastname() {
        return lastname;
    }

    public Gender getGender() {
        return gender;
    }

    @Override
    public String toString() {
        return "Student{" + "schoolId=" + schoolId
                + ", firstname='" + firstname + '\''
                + ", lastname='" + lastname + '\'' + '}';
    }
}

好吧,过滤很酷,但是很慢。所以如果你想要一些性能 - 让我们添加索引:

Map<String,Set<StudentKey>> lastNamesMap = new HashMap<>();

Set<StudentKey> getByLastName(String lastName) 
  return lastNamesMap.containsKey(s1.getLastname()) ? lastNamesMap.get(s1.getLastname()) : Collections.emptySet();  
}

void addStudent(Student s1) {
 final String k = s1.getLastname();
 Set<StudentKey> keys;
 if (lastNamesMap.containsKey(k)) {
  keys = lastNamesMap.get(k);
 } else {
  keys = new TreeSet<>();
 }
 StudentKey key = new StudentKey(s1.getSchoolId(), s1.getLastname();
 keys.add(key);
 lastNamesMap.put(k,keys);
 schoolsMap.put(key, s1);
 }