Java学习笔记——Set、List集合
标签: Java 集合(Set、List)
文章目录
(一)Set
Set集合是最简单的一种集合,集合中的对象无序、不能重复。- 用得较多的主要实现类有
HashSet和TreeSet。HashSet:按照哈希算法来存取集合中的对象,存取速度比较快。TreeSet:实现SortedSet接口,具有排序功能。
(1)HashSet
Set集合没有重复对象,当一个新的对象加入到HashSet集合中时,HashSet的add方法会先判断是否存在这个对象,如果不是则添加到集合中。判断过程:先遍历集合,判断集合中有没有一个对象的HashCode与待添加的对象的HashCode相等,如果没有,则将待添加对象添加到集合中,如果有,则进一步判断equals,如果该对象与待添加对象的equals比较结果为false,则将待添加对象添加到集合中。
来看一个例子?:
String s1 = new String("123");
String s2 = new String("123");
Set<String> set = new HashSet<>();
set.add(s1);
set.add(s2);
set.forEach(System.out::println); // Java8新特性
结果为"123",说明s2并没有加入到set中,这是为什么呢,看下面给出的JDK源码,我们知道,String类是重写了Object类的hashCode方法和equals方法的,首先s1和s2的值都为"123",通过下面的源码可以得出结论,它们的hashCode相等,equals(重写之后变为比较它们的值)为true,即视为同一个对象。如果再加入一个到集合中:
String s3 = "123";
结果还是一个"123",道理一样。
//JDK String类的hashCode源码
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
//JDK String类的equals源码
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
再看一个例子?:
这次我们自己新建一个Student类:
class Student {
private String name;
private int age;
public Student(String name, int age) {
super();
this.name = name;
this.age = age;
}
public Student() {
super();
}
@Override
public String toString() {
return "Student [name=" + name + ", age=" + age + "]";
}
}
然后再执行下面代码:
Set<Student> set1 = new HashSet<>();
Student student1 = new Student("hzj", 1);
Student student2 = new Student("hzj", 1);
set1.add(student1);
set1.add(student2);
new HashSet<>().add(student1);
set1.forEach(System.out::println);
结果为Student [name=hzj, age=1] Student [name=hzj, age=1],有两个“一样的”值,原因很显然,new了两个对象,那么它们的hashCode值肯定不同。下面给Student类重写hashCode方法:
@Override
public int hashCode() {
return name.hashCode() + age;
}
添加之后结果还是两个,因为它们的equals结果为false,下面再给Student类重写equals方法:
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj instanceof Student) {
Student student = (Student) obj;
if (student.name.equals(name) && student.age == age) {
return true;
}
}
return false;
}
这样,结果就为一个了。
(2)TreeSet
TreeSet实现了SortedSet接口,能够对集合中的对象进行排序。当TreeSet向集合中加入一个对象时,会把它插入到有序的对象序列中。TreeSet支持两种排序方式:自然排序和客户化排序。默认情况下TreeSet采用的是自然排序方式。
- 自然排序
使要排序的类实现Comparable接口并实现compareTo方法。例如:
class Student implements Comparable<Student>{
private String name;
private int age;
public Student(String name, int age) {
super();
this.name = name;
this.age = age;
}
public Student() {
super();
}
@Override
public String toString() {
return "Student [name=" + name + ", age=" + age + "]";
}
@Override
public int compareTo(Student o) { // 通过名字的升序排列
return name.compareTo(o.name);
}
}
// 测试代码
Set<Student> set1 = new TreeSet<>();
Student student1 = new Student("b", 1);
Student student2 = new Student("a", 2);
Student student3 = new Student("c", 0);
set1.add(student1);
set1.add(student2);
set1.add(student3);
new HashSet<>().add(student1);
set1.forEach(System.out::println);
/*结果:名字升序
Student [name=a, age=2]
Student [name=b, age=1]
Student [name=c, age=0]
*/
- 客户化排序
java.util.Comparator接口提供了具体的排序方法, 它有一个compare(Object x, Object y)方法,用于比较两个对象的大小。
此方式下的Student类不用实现Comparable接口,而是新建一个比较类,让这个类实现Comparator接口,详细代码:
package com.birup.hdjd.chap06;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Set;
import java.util.TreeSet;
public class CollectionTest {
public static void main(String[] args) {
Set<Student> set1 = new TreeSet<>(new StudentComparator());//注意这里与上面自然排序的区别,使用的构造器不一样
Student student1 = new Student("b", 1);
Student student2 = new Student("a", 2);
Student student3 = new Student("c", 0);
set1.add(student1);
set1.add(student2);
set1.add(student3);
new HashSet<>().add(student1);
set1.forEach(System.out::println);
}
/*结果:
Student [name=c, age=0]
Student [name=b, age=1]
Student [name=a, age=2]
*/
}
class StudentComparator implements Comparator<Student>{
@Override
public int compare(Student o1, Student o2) {
return o1.getAge() - o2.getAge(); // 年龄升序
}
}
class Student{
private String name;
private int age;
public Student(String name, int age) {
super();
this.name = name;
this.age = age;
}
public Student() {
super();
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Student [name=" + name + ", age=" + age + "]";
}
}
(二)List
- List的遍历方式有两种:
a.list.get(i); //通过索引检索对象;
b.Iterator it = list.iterator(); //通过迭代器
it.next();
- 添加对象:
add(object)- 获取对象:
get(index)- 删除对象:
remove(index|object)- 判断是否存在对象:
contains(Object)
可以用Arrays.asList()将一个数组转化为一个List对象:
Integer[] a = new Integer[] {1, 3, 2, 5};
List<Integer> asList = Arrays.asList(a);
asList.forEach(System.out::println);
/*结果:
1
3
2
5
*/
也可以用Collection.toArray()将一个集合转换成一个数组对象:
List<Integer> asList = Arrays.asList(1, 2, 3, 4);
Object[] array = asList.toArray();
for (Object object : array)
System.out.println(object.toString());
/*结果:
1
2
3
4
*/
(1)ArrayList
ArrayList代表长度可变的数组。ArrayList允许对元素进行快速的随机访问,但是向ArrayList中插入与删除元素的速度较慢。
使用方式:
List<Integer> list = new ArrayList<>();
list.add(11); // 添加
list.add(22);
list.remove(0); // 删除remove(index|object)
list.get(0); // 取值get(index)
(2)LinkedList
LinkedList插入是比较快的,因为不用移动元素。
使用方式:
List<Integer> list = new LinkedList<>();
list.add(11); // 添加
list.add(22);
list.remove(0); // 删除remove(index|object)
list.get(0); // 取值get(index)
(3)ArrayList与LinkedList比较
LinkedList和ArrayList的差别主要来自于Array和LinkedList数据结构的不同。Array是基于索引(index)的数据结构,它使用索引在数组中搜索和读取数据是很快的。但是要删除数据却是开销很大的,因为这需要重排数组中的所有数据。- 相对于
ArrayList,LinkedList插入是更快的。因为LinkedList不像ArrayList一样,不需要改变数组的大小,也不需要在数组装满的时候要将所有的数据重新装入一个新的数组,这是ArrayList最坏的一种情况。LinkedList需要更多的内存。
我们构建以下程序来测试它们俩的取值和添值速度:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
public class CollectionTest {
private static final int N = 50000;
private static List<Integer> val = null;
static { // 初始化val
Integer[] integers = new Integer[N];
for (int i = 0; i < N; ++i) {
integers[i] = (int) (Math.random() * 100);
}
val = Arrays.asList(integers);
}
public static Long getTime(List<Integer> list) { // 测试取值所消耗的时间
Long start = System.currentTimeMillis(); // 开始时间
for (int i = 0; i < N; ++i) {
list.get(i);
}
Long end = System.currentTimeMillis(); // 结束时间
return end - start;
}
public static Long addTime(List<Integer> list) { // 测试添加所消耗的时间
Long start = System.currentTimeMillis(); // 开始时间
for (int i = 0; i < N; ++i) {
list.add(1, (int) (Math.random() * 100));
}
Long end = System.currentTimeMillis(); // 结束时间
return end - start;
}
public static void main(String[] args) {
System.out.println("ArrayList(get):" + getTime(new ArrayList<Integer>(val)) + "ms");
System.out.println("LinkedList(get):" + getTime(new LinkedList<Integer>(val)) + "ms");
System.out.println("ArrayList(add):" + addTime(new ArrayList<Integer>(val)) + "ms");
System.out.println("LinkedList(add):" + addTime(new LinkedList<Integer>(val)) + "ms");
}
}
/*结果:
ArrayList(get):2ms
LinkedList(get):1015ms
ArrayList(add):896ms
LinkedList(add):5ms
*/
由结果很容易得出结论:ArrayList取值比LinkedList取值快很多,LinkedList添值比ArrayList快很多。
但是,如果添加值的index比较大,这个时候LinkedList就没有任何优势了,我们修改以上代码的addTime方法:
public static Long addTime(List<Integer> list) { // 测试添加所消耗的时间
Long start = System.currentTimeMillis(); // 开始时间
for (int i = 0; i < N; ++i) {
list.add(i, (int) (Math.random() * 100)); // 注意第一个参数变为i
}
Long end = System.currentTimeMillis(); // 结束时间
return end - start;
}
/*结果:
ArrayList(add):566ms
LinkedList(add):1895ms
*/
结果LinkedList比ArrayList慢了很多,为什么呢,因为LinkedList要先找到index这个位置,一旦这个index过大,就比较费时了,这就是为什么上面结果会这么慢了。
使用集合需要注意的地方
- 由于Java集合存放的是对象的引用,循环使用
add()方法需要注意,改变一个对象的值可能会改变集合中对象的值。
看如下一段代码,猜猜运行结果:
import java.util.ArrayList;
import java.util.Collection;
public class Test {
public static void main(String[] args) {
Collection<Student> c = new ArrayList<>();
Student s = new Student("hzj", 10);
for(int i = 0; i < 10; ++i) {
s.setAge(i);
c.add(s);
}
c.forEach(System.out::println); // 结果?
}
}
class Student { // 实体类
private String name;
private int age;
public Student() {
super();
}
public Student(String name, int age) {
super();
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Student [name=" + name + ", age=" + age + "]";
}
}
有没有人觉得会是如下结果:
Student [name=hzj, age=0]
Student [name=hzj, age=1]
Student [name=hzj, age=2]
Student [name=hzj, age=3]
Student [name=hzj, age=4]
Student [name=hzj, age=5]
Student [name=hzj, age=6]
Student [name=hzj, age=7]
Student [name=hzj, age=8]
Student [name=hzj, age=9]
正确的结果其实是这样的:
Student [name=hzj, age=9]
Student [name=hzj, age=9]
Student [name=hzj, age=9]
Student [name=hzj, age=9]
Student [name=hzj, age=9]
Student [name=hzj, age=9]
Student [name=hzj, age=9]
Student [name=hzj, age=9]
Student [name=hzj, age=9]
Student [name=hzj, age=9]
为什么是这个结果呢,因为Java集合存放的是对象的引用,并不是对象本身,所以当该Student对象更新之后,之前加入集合的对象的Student的值会因为该Student的对象更新而更新,但对象地址没有发生变化,所以当集合遍历的时候,由于是存放的地址,我们会取到同一个Student对象,而对象的值也更新成了最后一个循环所赋的值。
我们可以用一个简单的例子证明一下(Student类使用上面的):
Collection<Student> c = new ArrayList<>();
Collection<Student> c1 = new ArrayList<>();
Student s = new Student("hzj", 10);
c.add(s);
c1.add(s);
System.out.println("修改前:");
c.forEach(System.out::println);
c1.forEach(System.out::println);
for (Student student : c) {
student.setAge(1000);
}
System.out.println("修改后:");
c.forEach(System.out::println);
c1.forEach(System.out::println);
/*结果:
修改前:
Student [name=hzj, age=10]
Student [name=hzj, age=10]
修改后:
Student [name=hzj, age=1000]
Student [name=hzj, age=1000]
*/
修改c中的值,导致c1中也发生了变化,证明集合存放的是对象的引用,这个例子用的是List,其他集合也是一样的。
- 如果集合中存放的对象类型为不可变的(如
String、Long、Integer等),则上面结论的不成立。
看一下代码:
Collection<Integer> c1 = new ArrayList<>();
Collection<Integer> c2 = new ArrayList<>();
Integer i1 = new Integer(130);
c1.add(i1);
c2.add(i1);
c1.forEach(System.out::println);
c2.forEach(System.out::println);
for (Integer integer : c1) {
integer = new Integer(100);
}
c1.forEach(System.out::println);
c2.forEach(System.out::println);
结果如下,也就是之后的修改并没有影响到集合中的值,因为Integer是不可变类。
130
130
130
130
本文深入解析Java中的Set和List集合,包括HashSet、TreeSet、ArrayList和LinkedList的特点与使用场景,对比不同集合的性能,并探讨集合使用时的注意事项。


被折叠的 条评论
为什么被折叠?



