Java基础知识填坑继续。
Java 集合与泛型
数组 VS ArrayList
数组大概是我们学习任何语言时接触到的第一个集合。
String[] strs = new String[10];
strs[0] = "a";
strs[1] = "b";
List<String> lists = new ArrayList<>();
数组也是对象;相较于普通的数组,ArrayList在创建时不必指定大小,会在进行增删操作时动态的调整自己的大小。
数组与List相互转换
//数组转换为List
lists = Arrays.asList(strs);
//List 转 数组
strs = lists.toArray(strs);
将集合中的对象进行排序
使用Collections.sort()方法对集合中的对象进行排序的两种方式。
- 该对象实现了Comparable接口,明确指定了排序方式。
public class Student implements Comparable<Student>{
private String name;
private int id;
public Student(String name, int id) {
this.name = name;
this.id = id;
}
@Override
public int compareTo(Student student) {
//按照name(字符串值)从小到大排序
return name.compareTo(student.name);
}
}
这样,由Student对象构成的集合就可以使用Collections.sort()方法进行排序了。
List<Student> students = new ArrayList<>();
for(int i=0;i<10;i++) {
Student student = new Student(i + "-name", i);
students.add(student);
}
Collections.sort(students);
- 实现Comparator接口,动态定义排序方式。
private static class ComprareByName implements Comparator<Student> {
@Override
public int compare(Student student, Student t1) {
//按name 从大到小进行排序
return t1.getName().compareTo(student.getName());
}
}
这样就可以使用重载的sort方法进行排序
Collections.sort(students,new ComprareByName());
Comparator的compare实现方式会覆盖集合元素中默认的实现,也就说虽然Student内部是从小到大排序,但会被这里ComprareByName的实现所覆盖。
为了使集合有序,我们也可以使用TreeSet。
TreeSet 以有序的状态存储元素,并防止重复。
因此,为了正确的使用TreeSet 需要注意以下两点:
- 同上面第一点,加入到TreeSet集合中对象(元素)必须实现了Comparable接口。
- 使用重载,用Comparator参数的构造函数来创建TreeSet。
TreeSet<Student> mStudents = new TreeSet<>(new ComprareByName());
集合分类
- Collections
- List 索引位置明确的集合
- Set 不允许重复元素的集合
- Map 使用成对key和value的集合
以上三种集合的类图如下:
从中可以看到我们常用的一些类,如ArrayList,HashMap 等。
如何检查对象的重复性?如何判定两个对象相等?
Student a = new Student("a", 1);
Student b = new Student("b", 2);
Student c = a;
Student d = new Student("b", 2);
System.err.println("a.hashCode()="+a.hashCode());
System.err.println("b.hashCode()="+b.hashCode());
System.err.println("c.hashCode()="+c.hashCode());
System.err.println("d.hashCode()="+d.hashCode());
输出
a.hashCode()=1118140819
b.hashCode()=1975012498
c.hashCode()=1118140819
d.hashCode()=1808253012
通过打印a,b,c,d 四个对象的hashcode 值,可以看到引用变量a和c 指向的是堆上的同一个对象,因此他们的hash值必然是相等的。引用对象b和d虽然创建的对象内容是一致的,但他们任然是分别指向两个不同的对象,因此hash值也是不同的。
下面我们用equals 方法比较一下这四个对象
System.out.println("a.equals(b) " + a.equals(b));
System.out.println("a.equals(c) " + a.equals(c));
System.out.println("b.equals(d) " + b.equals(d));
输出
a.equals(b) false
a.equals(c) true
b.equals(d) false
结果很明显,因为Object的equals 默认执行的是对象引用是否相等的比较,因此b.equals(d)的结果为false。
public boolean equals(Object obj) {
return (this == obj);
}
但是,从我们创建对象的代码可以得知,b和d 这两个对象内容是一样的;因此,这两个对象就应该是同一个,他们应该是相等的;因此,我们可以覆盖默认的hashCode()和equals()方法。
public class Student implements Comparable<Student>{
private String name;
private int id;
@Override
public int hashCode() {
return name.hashCode();
}
@Override
public boolean equals(Object o) {
Student mStudent= (Student) o;
return getName().equals(mStudent.getName());
}
public String getName() {
return name;
}
}
现在再次测试,就可以看到b.equals(d)的结果为true了。
因此,我们可以得出以下结论:
- 如果两个对象相等,则hashcode值必须相等
- 两个对象的hashcode值相等,他们也不一定是相等的。(当然,这种几率应该很小)
- equals()默认执行的是== 比较,也就是说会去测试两个引用是否对堆上同一个对象引用;因此,对于我们自己创建的对象,应该同时覆盖equals()方法和hashCode()方法,规范检测对象一致性的标准。
泛型
使用泛型可以构建出类型更加安全的集合,让问题尽可能的在编译期就被发现,而不是等到了执行期才冒出来
public class ArrayList<E> extends AbstractList<E> implements List<E>
以常用的ArrayList为例,使用泛型后代表以后所有对ArrayList的操作,添加,删除或返回的对象类型都是E,不能是其他类型。
List<Student> students = new ArrayList<>();
for(int i=0;i<10;i++) {
Student student = new Student(i + "-name", i);
students.add(student);
}
students 中添加的元素只能是Student类型的,不能是其他;同时从students结合中获取到的对象也一定是Student类型。
从泛型的角度看,extends和implements是等价的,都表示当前类是一个……。对ArrayList来说,他既是一个AbstractList,也是List。
当泛型遇到多态
现在有Student的子类:SeniorStudent和CollegeStudent。
private static void printStudents(List<Student> students) {
for (Student mStudent : students) {
System.out.println(mStudent.getName());
}
//面对这样的情况,这个方法只能接受List<Student>类型的参数
students.add(new SeniorStudent("hacker", 999));
}
public static void main(String[] args) {
ArrayList<Student> students = new ArrayList<>();
//因为泛型,List现在可以接受所有Student类型的对象
students.add(new Student("mike",001));
students.add(new CollegeStudent("lucy",002));
students.add(new SeniorStudent("tom", 003));
//
printStudents(students);
List<CollegeStudent> colleges = new ArrayList<>();
colleges.add(new CollegeStudent("a", 100));
colleges.add(new CollegeStudent("b", 101));
colleges.add(new CollegeStudent("c", 102));
//这样做是不行的
printStudents(colleges);
}
在上面的代码中,printStudents(List<Student> students),我们可以这样
printStudents(ArrayList<Student>)
但却不能这样
printStudents(List<CollegeStudent>)
在泛型方法中,参数中的集合可以是多态,但集合中的对象不能是多态。其中的道理我们通过printStudents 方法中最后一行语句很容易理解。因为你不能保证使用集合的方法,会对集合做怎样的操作。为了保证集合的安全性,这是很好的做法。
但是,这样不就丧失了多态的意义吗?如果不能用子类作为集合的类型,那难道要为每一个Student的子类型,单独写一个printStudents()方法吗? 其实不必,只要做如下改动即可:
private static void printStudents(List<? extends Student> students) {
for (Student mStudent : students) {
System.out.println(mStudent.getName());
}
//当使用通配符声明后,将不能再向集合中添加元素,因此以下语句非法
students.add(new SeniorStudent("hacker", 999));
}
这种情况虽然从语法角度看似合理,但编译器会帮我们做限制,限制再次修改集合中的元素
这样printStudents(List<CollegeStudent>)就变得合法了。
当然,为了更容易理解,也可以这样声明:
private static <T extends Student> void printStudents(List<T> students) {
for (Student mStudent : students) {
System.out.println(mStudent.getName());
}
//当使用通配符声明后,将不能再向集合中添加元素,因此以下语句非法
students.add(new SeniorStudent("hacker", 999));
}