Java集合
文章目录
0. 热身
List
package lession11_26;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* @Classname ListMain
* @Description TODO
* @Date 2021/11/26 13:25
* @Created by DELL
*/
class SuperMan {
private String name;
private String idcard;
private int age;
private char sex;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getIdcard() {
return idcard;
}
public void setIdcard(String idcard) {
this.idcard = idcard;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public char getSex() {
return sex;
}
public void setSex(char sex) {
this.sex = sex;
}
@Override
public String toString() {
return "SuperMan{" +
"name='" + name + '\'' +
", idcard='" + idcard + '\'' +
", age=" + age +
", sex=" + sex +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SuperMan superMan = (SuperMan) o;
if (age != superMan.age) return false;
if (sex != superMan.sex) return false;
if (name != null ? !name.equals(superMan.name) : superMan.name != null) return false;
return idcard != null ? idcard.equals(superMan.idcard) : superMan.idcard == null;
}
// @Override
// public int hashCode() {
// int result = name != null ? name.hashCode() : 0;
// result = 31 * result + (idcard != null ? idcard.hashCode() : 0);
// result = 31 * result + age;
// result = 31 * result + (int) sex;
// return result;
// }
}
public class ListMain {
public static void main(String[] args) {
ArrayList arr = new ArrayList(10);
// 操作Java提供的常用类。
Integer a = 1;
Boolean b = true;
String str = "123456";
arr.add(a);
arr.add(b);
arr.add(str);
arr.get(0);
arr.get(1);
arr.get(2);
arr.remove(true);
arr.set(0, 500);
// arr.set(10,800); 报错
//往集合中添加SuperMan元素
SuperMan superMan = new SuperMan();
superMan.setName("绿巨人");
superMan.setAge(18);
superMan.setSex('男');
superMan.setIdcard("1234567");
arr.add(superMan);
for (int i = 1; i <= 10; i++) {
SuperMan man = new SuperMan();
man.setName("绿巨人"+i);
man.setAge(i);
man.setSex('男');
man.setIdcard("1928424"+"编号"+i);
arr.add(man);
}
SuperMan man = new SuperMan();
man.setName("绿巨人"+1);
man.setAge(1);
man.setSex('男');
man.setIdcard("1928424"+"编号"+1);
arr.add("sdihfkdsjfhkjs");
System.out.println(arr.contains(man));
System.out.println(arr);
//使用泛型,规定集合中所存储的数据类型
ArrayList<SuperMan> arrs = new ArrayList<>();
for (int i = 1; i <= 10; i++) {
SuperMan man1 = new SuperMan();
man1.setName("绿巨人" + i);
man1.setAge(i);
man1.setSex('男');
man1.setIdcard("1928424" + "编号" + i);
arrs.add(man1);
}
//移除集合中元素时,注意集合中元素的移动
for (int i = 0; i <arrs.size(); i++) {
if (arrs.get(i).getAge()==5){
arrs.remove(i);
}
if (arrs.get(i).getAge()==6){
System.out.println("这是6岁的绿巨人");
}
}
//使用stream流操作结合
arrs.forEach(System.out::println);
List<String> collect = arrs.stream()
.sorted((v1,v2)->v2.getAge()-v1.getAge())
.filter(e->e.getAge()>5)
.map(SuperMan::getIdcard)
.collect(Collectors.toList());
collect.forEach(System.out::println);
System.out.println(collect);
//不使用stream
ArrayList<String> superManIds=new ArrayList<>();
for (int i = 0; i < arrs.size(); i++) {
SuperMan man1 = arrs.get(i);
if (man1.getAge()>5){
superManIds.add(man1.getIdcard());
}
}
superManIds.forEach(System.out::println);
}
}
Map
public class MapTest {
public static void main(String[] args) {
Map<Integer, String> map = new HashMap<>();
map.put(1, "小米");
map.put(2, "honor");
map.put(2, "华为");
map.put(3, "redmi");
//替换
map.replaceAll((v1,v2)->v1+String.valueOf((v2.length())));
System.out.println(map);
//删除
map.remove(2);
System.out.println(map.containsValue("redmi"));
//遍历
map.keySet().forEach(e -> System.out.println(map.get(e)));
//合并
map.merge(1, "applephone", (v1, v2) -> {
return String.valueOf(((String) v1).length() + ((String) v2).length());
});
//遍历
for (Integer key : map.keySet()
) {
System.out.println(map.get(key));
}
}
}
集合体系
集合类和数组不一样,数组元素既可以是基本类型的值,也可以是对象(实际上保存的是对象的引用变量)。
集合里只能保存对象(实际上只是保存对象的引用变量,但通常习惯上认为集合里保存的是对象)。
Java的集合类主要由两个接口派生而出:Collection和 Map,Collection和 Map是Java集合框架的根接口,这两个接口又包含了一些子接口或实现类。
Map体系
1. Collection和Iterator接口
Collection介绍
Collection接口是List、Set和 Queue接口的父接口,该接口里定义的方法既可用于操作Set集合,也可用于操作List和 Queue集合。
- boolean add(Object o):该方法用于向集合里添加一个元素。如果集合对象被添加操作改变了,则返回true。
- boolean addAll(Collection c):该方法把集合c里的所有元素添加到指定集合里。如果集合对象被添加操作改变了,则返回true。
- void clear():清除集合里的所有元素,将集合长度变为0。
- boolean contains(Object o):返回集合里是否包含指定元素
- boolean containsAll(Collection c):返回集合里是否包含集合c里的所有元素。
- boolean isEmpty():返回集合是否为空。当集合长度为О时返回true,否则返回false。
- Iterator iterator():返回一个Iterator对象,用于遍历集合里的元素。
- boolean remove(Object o):删除集合中的指定元素o,当集合中包含了一个或多个元素o时,该方法只删除第一个符合条件的元素,该方法将返回true。
- boolean removeAll(Collection c):从集合中删除集合c里包含的所有元素(相当于用调用该方法的集合减集合c),如果删除了一个或一个以上的元素,则该方法返回true。
- boolean retainAll(Collection c):从集合中删除集合c里不包含的元素(相当于把调用该方法的集合变成该集合和集合c的交集),如果该操作改变了调用该方法的集合,则该方法返回true。
- int size():该方法返回集合里元素的个数。
- Object [] toArray():该方法把集合转换成一个数组,所有的集合元素变成对应的数组元素。
- 详情查看api文档
集合练习
public class CollectionTest
{
public static void main(String[] args)
{
Collection c = new ArrayList();
// 添加元素
c.add("孙悟空");
// 虽然集合里不能放基本类型的值,但Java支持自动装箱
c.add(6);
System.out.println("c集合的元素个数为:" + c.size()); // 输出2
// 删除指定元素
c.remove(6);
System.out.println("c集合的元素个数为:" + c.size()); // 输出1
// 判断是否包含指定字符串
System.out.println("c集合的是否包含\"孙悟空\"字符串:"
+ c.contains("孙悟空")); // 输出true
c.add("轻量级Java EE企业应用实战");
System.out.println("c集合的元素:" + c);
Collection books = new HashSet();
books.add("轻量级Java EE企业应用实战");
books.add("疯狂Java讲义");
System.out.println("c集合是否完全包含books集合?"
+ c.containsAll(books)); // 输出false
// 用c集合减去books集合里的元素
c.removeAll(books);
System.out.println("c集合的元素:" + c);
// 删除c集合里所有元素
c.clear();
System.out.println("c集合的元素:" + c);
// 控制books集合里只剩下c集合里也包含的元素
books.retainAll(c);
System.out.println("books集合的元素:" + books);
}
}
当使用System.out的printIn()方法来输出集合对象时,将输出[ele1,ele2…].[的形式,这显然是因为所有的Collection实现类都重写了toString()方法,该方法可以一次性地输出集合中的所有元素。
注意:如果不为集合设置泛型,那么默认情况下,集合存储的数据类型为Object类型
遍历集合
1. 使用Lambda遍历集合
Java 8为Iterable接口新增了一个forEach(Consumer action)默认方法,该方法所需参数的类型是一
个函数式接口,而Iterable接口是Collection接口的父接口,因此Collection集合也可直接调用该方法。
当程序调用Iterable 的forEach(Consumer action)遍历集合 元素时,程序会依次将集合元素传给
Consumer的accept(T t)方法(该接口中唯一的抽象方法)。 正因为Consumer是函数式接口,因此可以
使用Lambda表达式来遍历集合元素。
如下程序示范了使用Lambda表达式来遍历集合元素。
public class CollectionEach
{
public static void main(String[] args)
{
// 创建一个集合
Collection books = new HashSet();
books.add("Java");
books.add("Js");
books.add("C++");
// 调用forEach()方法遍历集合
books.forEach(obj -> System.out.println("迭代集合元素:" + obj));
}
}
2. 使用Iterator遍历集合元素
Collection系列集合、Map系列集合主要用于盛装其他对象,而 Iterator则主要用于遍历〈即迭代访问)Collection集合中的元素,Iterator对象也被称为迭代器。
Iterator接口隐藏了各种Collection实现类的底层细节,向应用程序提供了遍历Collection集合元素的统一编程接口。
定义的方法:
-
boolean hasNext():如果被迭代的集合元素还没有被遍历完,则返回true。
-
Object next():返回集合里的下一个元素。
-
void remove():删除集合里上一次next方法返回的元素。
-
void forEachRemaining(Consumer action),这是Java 8为lterator新增的默认方法,该方法可使用Lambda表达式来遍历集合元素。
public class IteratorTest
{
public static void main(String[] args)
{
Collection books = new HashSet();
books.add("Java");
books.add("Js");
books.add("C++");
// 获取books集合对应的迭代器
Iterator it = books.iterator();
while(it.hasNext())
{
// it.next()方法返回的数据类型是Object类型,因此需要强制类型转换
String book = (String)it.next();
System.out.println(book);
if (book.equals("Java"))
{
// 从集合中删除上一次next方法返回的元素
it.remove();
}
// 对book变量赋值,不会改变集合元素本身
book = "测试字符串"; //无效
}
System.out.println(books);
}
}
- Iterator必须依附于Collection对象,若有一个Iterator对象,则必然有一个与之关联的Collection对象。Iterator提供了两个方法来迭代访问Collection集合里的元素,并可通过remove()方法来删除集合中上一次next()方法返回的集合元素。
- 对迭代变量book进行赋值,但当再次输出books 集合时,会看到集合里的元素没有任何改变。当使用Iterator对集合元素进行迭代时,Iterator 并不是把集合元素本身传给了迭代变量,而是把集合元素的值传给了迭代变量,所以修改迭代变量的值对集合元素本身没有任何影响。
- 当使用Iterator 迭代访问Collection 集合元素时,Collection 集合里的元素不能被改变,只有通过Iterator的remove0方法删除上一次next0方法返回的集合元素才可以;否则将会引发java.util.Concurrent
ModificationException异常。
public class IteratorErrorTest
{
public static void main(String[] args)
{
Collection books = new HashSet();
// 创建集合、添加元素的代码与前一个程序相同
books.add("Java");
books.add("Js");
books.add("C++");
// 获取books集合对应的迭代器
Iterator it = books.iterator();
while(it.hasNext())
{
String book = (String)it.next();
System.out.println(book);
if (book.equals("Java"))
{
// 使用Iterator迭代过程中,不可修改集合元素,下面代码引发异常
books.remove(book);
}
}
}
}
注意:当我们进行集合迭代时,删除元素会导致错误。
3. 使用Lambda表达式遍历Iterator
Java 8为Iterator新增了一个forEachRemaining(Consumer action)方法,该方法所需的Consumer参
数同样也是函数式接口。当程序调用Iterator的forEachRemaining(Consumer action)遍历集合元素时,程序会依次将集合元素传给Consumer的accept(T t)方法( 该接口中唯一的抽象方法)。
public class IteratorEach
{
public static void main(String[] args)
{
// 创建集合、添加元素的代码与前一个程序相同
Collection books = new HashSet();
books.add("Java");
books.add("Js");
books.add("C++");
// 获取books集合对应的迭代器
Iterator it = books.iterator();
// 使用Lambda表达式(目标类型是Comsumer)来遍历集合元素
it.forEachRemaining(obj -> System.out.println("迭代集合元素:" + obj));
}
}
4. 使用foreach循环遍历集合元素
public class ForeachTest
{
public static void main(String[] args)
{
// 创建集合、添加元素的代码与前一个程序相同
Collection books = new HashSet();
books.add("Java");
books.add("Js");
books.add("C++");
for (Object obj : books)
{
// 此处的book变量也不是集合元素本身
String book = (String)obj;
System.out.println(book);
if (book.equals("疯狂Android讲义"))
{
// 下面代码会引发ConcurrentModificationException异常
books.remove(book); //①
}
}
System.out.println(books);
}
}
5. 使用Predicate操作集合
Java 8为Collection集合新增了一个个removelf(Predicate filter)方法,该方法将会批量删除符合filter
条件的所有元素。该方法需要一个Predicate (谓词)对象作为参数,Predicate 也是函数式接口,因此可
使用Lambda表达式作为参数。
removeIf,删除符合条件的数据,条件通过实现Predicate 的方法来创造。
Predicate :类似筛选条件。
如下程序示范了使用Predicate 来过滤集合。
public class PredicateTest
{
public static void main(String[] args)
{
// 创建一个集合
Collection books = new HashSet();
books.add(new String("Java EE"));
books.add(new String("Java"));
books.add(new String("iOS"));
books.add(new String("Ajax"));
books.add(new String("Android"));
// 使用Lambda表达式(目标类型是Predicate)过滤集合
books.removeIf(ele -> ((String)ele).length() < 5);
System.out.println(books);
}
}
需求:
- 统计书名中出现“疯狂”字符串的图书数量。
- 统计书名中出现“Java”字符串的图书数量。
- 统计书名长度大于10的图书数量。
public class PredicateTest2
{
public static void main(String[] args)
{
// 创建books集合、为books集合添加元素的代码与前一个程序相同。
Collection books = new HashSet();
books.add(new String("轻量级Java EE企业应用实战"));
books.add(new String("疯狂Java讲义"));
books.add(new String("疯狂iOS讲义"));
books.add(new String("疯狂Ajax讲义"));
books.add(new String("疯狂Android讲义"));
// 统计书名包含“疯狂”子串的图书数量
System.out.println(calAll(books , ele->((String)ele).contains("疯狂")));
// 统计书名包含“Java”子串的图书数量
System.out.println(calAll(books , ele->((String)ele).contains("Java")));
// 统计书名字符串长度大于10的图书数量
System.out.println(calAll(books , ele->((String)ele).length() > 10));
}
public static int calAll(Collection books , Predicate p)
{
int total = 0;
for (Object obj : books)
{
// 使用Predicate的test()方法判断该对象是否满足Predicate指定的条件
if (p.test(obj))
{
total ++;
}
}
return total;
}
}
6. 使用Stream流
Java 8新增了Stream、IntStream、 LongStream、 DoubleStream 等流式API,这些API代表多个支持串行和并行聚集操作的元素。
上面4个接口中,Stream是一个通用的流接口,而IntStream、LongStream、DoubleStream则代表元素类型为int、long、 double 的流。
每个流式API有对应的Builder, 例如Stream.Builder、 IntStream.Builder、LongStream.Builder、DoubleStream. Builder,开发者可以通过这些Builder来创建对应的流。
使用Stream的步骤如下:
- 使用Stream或XxxStream的builder()类方法创建该Stream对应的Builder。
- 重复调用Builder的add()方法向该流中添加多个元素。
- 调用Builder的build()方法获取对应的Stream。
- 调用Stream的聚集方法。
public class IntStreamTest
{
public static void main(String[] args)
{
IntStream is = IntStream.builder()
.add(20)
.add(13)
.add(-2)
.add(18)
.build();
// 下面调用聚集方法的代码每次只能执行一个
System.out.println("is所有元素的最大值:" + is.max().getAsInt());
System.out.println("is所有元素的最小值:" + is.min().getAsInt());
System.out.println("is所有元素的总和:" + is.sum());
System.out.println("is所有元素的总数:" + is.count());
System.out.println("is所有元素的平均值:" + is.average());
System.out.println("is所有元素的平方是否都大于20:"
+ is.allMatch(ele -> ele * ele > 20));
System.out.println("is是否包含任何元素的平方大于20:"
+ is.anyMatch(ele -> ele * ele > 20));
// 将is映射成一个新Stream,新Stream的每个元素是原Stream元素的2倍+1
IntStream newIs = is.map(ele -> ele * 2 + 1);
// 使用方法引用的方式来遍历集合元素
newIs.forEach(System.out::println); // 输出41 27 -3 37
}
}
上面程序先创建了一个IntStream,接下来调用IntStream的聚集方法执行操作,这样即可获取该流的相关信息。
注意:一个IntStream调用一次聚集方法后,流就进行了关闭,不能调用其他的方法。
Stream提供了大量的方法进行聚集操作,这些方法既可以是“中间的”(intermediate),也可以是“末端的”(terminal)。
- 中间方法:中间操作允许流保持打开状态,并允许直接调用后续方法。上面程序中的map()方法就是中间方法。中间方法的返回值是另外一个流。
- 末端方法:末端方法是对流的最终操作。当对某个Stream执行末端方法后,该流将会被“消耗”且不再可用。上面程序中的sum()、count()、 averageO等 方法都是末端方法。
流的方法还有如下两个特征。
- 有状态的方法:这种方法会给流增加一些新的属性,比如元素的唯一性、元素的最大数量、保证元素以排序的方式被处理等。有状态的方法往往需要更大的性能开销。
- 短路方法:短路方法可以尽早结束对流的操作,不必检查所有的元素。
常用的中间方法
- filter(Predicate predicate):过滤Stream中所有不符合predicate 的元素
- mapToXxx(ToXxxFunction mapper):使用ToXxxFunction对流中的元素执行一对一的转换,该方法返回的新流中包含了ToXxxFunction转换生成的所有元素。
- peek(Consumer action):依次对每个元素执行一些操作, 该方法返回的流与原有流包含相同的元素。该方法主要用于调试。
- distinct(): 该方法用于排序流中所有重复的元素(判断元素重复的标准是使用equals)比较返回true。这是一个有状态的方法。
- sorted(): 该方法用于保证流中的元素在后续的访问中处于有序状态。这是一个有状态的方法。
- limit(long maxSize):该方法用于保证对该流的后续访问中最大允许访问的元素个数。这是一个有状态的、短路方法。
常用的末端方法
- forEach(Consumer action):遍历流中所有元素,对每个元素执行action。
- toArray(): 将流中所有元素转换为一个数组。
- reduce(): 该方法有三个重载的版本,都用于通过某种操作来合并流中的元素。
- min(): 返回流中所有元素的最小值。
- max(): 返回流中所有元素的最大值。
- count():返回流中所有元素的数量。
- anyMatch(Predicate predicate):判断流中是否至少包含一个元素符合Predicate条件。
- allMatch(Predicate predicate):判断流中是否每个元素都符合Predicate条件。
- noneMatch(Predicate predicate):判断流中是否所有元素都不符合Predicate条件。
- findFirst():返回流中的第一个元素。
- findAny():返回流中的任意一个元素。
public class CollectionStream
{
public static void main(String[] args)
{
// 创建books集合、为books集合添加元素的代码与8.2.5小节的程序相同。
Collection books = new HashSet();
books.add(new String("轻量级Java EE企业应用实战"));
books.add(new String("疯狂Java讲义"));
books.add(new String("疯狂iOS讲义"));
books.add(new String("疯狂Ajax讲义"));
books.add(new String("疯狂Android讲义"));
// 统计书名包含“疯狂”子串的图书数量
System.out.println(books.stream()
.filter(ele->((String)ele)
.contains("疯狂"))
.count()); // 输出4
// 统计书名包含“Java”子串的图书数量
System.out.println(books.stream()
.filter(ele->((String)ele).contains("Java") )
.count()); // 输出2
// 统计书名字符串长度大于10的图书数量
System.out.println(books.stream()
.filter(ele->((String)ele).length() > 10)
.count()); // 输出2
// 先调用Collection对象的stream()方法将集合转换为Stream,
// 再调用Stream的mapToInt()方法获取原有的Stream对应的IntStream
books.stream().mapToInt(ele -> ((String)ele).length())
// 调用forEach()方法遍历IntStream中每个元素
.forEach(System.out::println);// 输出8 11 16 7 8
}
}
程序只要调用Collection的 stream()方法即可返回该集合对应的Stream,接下来就可通过Stream提供的方法对所有集合元素进行处理,这样大大地简化了集合编程的代码,这也是 Stream编程带来的优势。
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
mapToInt()方法就是一个中间方法,因此程序可继续调用IntStream的 forEach()方法来遍历流中的元素。
流的概念
class Dish {
private final String name;
private final boolean vegetarian;
private final int calories;
private final Type type;
public Dish(String name, boolean vegetarian, int calories, Type type) {
this.name = name;
this.vegetarian = vegetarian;
this.calories = calories;
this.type = type;
}
public String getName() {
return name;
}
public boolean isVegetarian() {
return vegetarian;
}
public int getCalories() {
return calories;
}
public Type getType() {
return type;
}
@Override
public String toString() {
return name;
}
public enum Type {MEAT, FISH, OTHER}
}
public class StreamTest {
public static void main(String[] args) {
List<Dish> menu = Arrays.asList(
new Dish("pork", false, 800, Dish.Type.MEAT),
new Dish("beef", false, 700, Dish.Type.MEAT),
new Dish("chicken", false, 400, Dish.Type.MEAT),
new Dish("french fries", true, 530, Dish.Type.OTHER),
new Dish("rice", true, 350, Dish.Type.OTHER),
new Dish("season fruit", true, 120, Dish.Type.OTHER),
new Dish("pizza", true, 550, Dish.Type.OTHER),
new Dish("prawns", false, 300, Dish.Type.FISH),
new Dish("salmon", false, 450, Dish.Type.FISH));
/*要求:
*1. 筛选出calories小于400的Dish
*2. 通过calories进行从小到大排序
*3. 获取符合以上两种条件的name集合。
*/
System.out.println("Java7");
//Java7遍历
List<Dish> lowCaloricDishes = new ArrayList<>();
for (Dish d : menu) {
if (d.getCalories() < 400) {
lowCaloricDishes.add(d);
}
}
Collections.sort(lowCaloricDishes, new Comparator<Dish>() {
public int compare(Dish d1, Dish d2) {
return Integer.compare(d1.getCalories(), d2.getCalories());
}
});
List<String> lowCaloricDishesName = new ArrayList<>();
for (Dish d : lowCaloricDishes) {
lowCaloricDishesName.add(d.getName());
}
lowCaloricDishes.forEach(System.out::println);
System.out.println("Java8");
//Java8
lowCaloricDishes.stream()
.filter(item->item.getCalories()<400)
.sorted(Comparator.comparing(Dish::getCalories))
.map(Dish::getName)
.collect(Collectors.toList());
lowCaloricDishes.forEach(System.out::println);
}
}
将流操作链接起来构成流的流水线.
2. List集合
List集合代表一个元素有序、可重复的集合,集合中每个元素都有其对应的顺序索引。可以通过索引来访问指定位置的集合元素。List作为Collection接口的子接口,可以使用Collection接口里的全部方法。
基本操作
常用方法
- void add(int index, Object element):将元素element插入到List集合的index 处。
- boolean addAll(int index,Collection c):将集合c所包含的所有元素都插入到List集合的 index处。
- Object get(int index):返回集合index索引处的元素。
- int indexOf(Object o):返回对象o在List集合中第一次出现的位置索引。
- int lastIndexOf(Object o):返回对象o在List集合中最后一次出现的位置索引。
- Object remove(int index):删除并返回index索引处的元素。
- Object set(int index, Object element):将index 索引处的元素替换成element对象,返回被替换的旧元素。
- List subList(int fromIndex, int toIndex):返回从索引fromIndex (包含)到索引toIndex (不包含)处所有集合元素组成的子集合。
所有的List实现类都可以调用这些方法来操作集合元素。与Set集合相比,List增加了根据索引来插入、替换和删除集合元素的方法。
public class ListTest
{
public static void main(String[] args)
{
List books = new ArrayList();
// 向books集合中添加三个元素
books.add(new String("轻量级Java EE企业应用实战"));
books.add(new String("疯狂Java讲义"));
books.add(new String("疯狂Android讲义"));
System.out.println(books);
// 将新字符串对象插入在第二个位置
books.add(1 , new String("疯狂Ajax讲义"));
for (int i = 0 ; i < books.size() ; i++ )
{
System.out.println(books.get(i));
}
// 删除第三个元素
books.remove(2);
System.out.println(books);
// 判断指定元素在List集合中位置:输出1,表明位于第二位
System.out.println(books.indexOf(new String("疯狂Ajax讲义")));
//将第二个元素替换成新的字符串对象
books.set(1, new String("疯狂Java讲义"));
System.out.println(books);
//将books集合的第二个元素(包括)到第三个元素(不包括)截取成子集合
System.out.println(books.subList(1 , 2));
}
}
问题:
books.add(1 , new String("疯狂Ajax讲义"));
// 判断指定元素在List集合中位置:输出1,表明位于第二位
System.out.println(books.indexOf(new String("疯狂Ajax讲义")));
-
我们往集合中添加了一个字符串对象“疯狂Ajax讲义”,但是我们又进行字符串新增时,添加的是一个新的字符串,两个字符串本质上并不属于同一个对象,但是我们使用books.indexOf(new String(“疯狂Ajax讲义”))时,却发现能返回字符串所在的位置1.
-
使用indexOf时或者remove()对象时,集合判断的标准是引用类型的equals方法。
-
class A { public boolean equals(Object obj) { return true; } } public class ListTest2 { public static void main(String[] args) { List books = new ArrayList(); books.add(new String("轻量级Java EE企业应用实战")); books.add(new String("疯狂Java讲义")); books.add(new String("疯狂Android讲义")); System.out.println(books); // 删除集合中A对象,将导致第一个元素被删除 books.remove(new A()); // System.out.println(books); // 删除集合中A对象,再次删除集合中第一个元素 books.remove(new A()); // System.out.println(books); } }
-
当我们删除一个集合中默认不存在的元素时,会默认删除集合中的第一个元素。
- 因为走的是A的equals方法,因此默认都返回true。
注意:
使用List的set(int index,Object element)方法时,可以改变集合指定索引处的元素,但是指定的索引只能是集合索引所在的范围呢,set方法并不能改变List集合的长度。比如集合长度为5,在6的地方添加一个元素是不行的。
Java8新增
- void replaceAll(UnaryOperator operator):根据 operator指定的计算规则重新设置List集合的所有元素。
- void sort(Comparator c):根据Comparator参数对List集合的元素排序。
public class ListTest3
{
public static void main(String[] args)
{
List books = new ArrayList();
// 向books集合中添加4个元素
books.add(new String("轻量级Java EE企业应用实战"));
books.add(new String("疯狂Java讲义"));
books.add(new String("疯狂Android讲义"));
books.add(new String("疯狂iOS讲义"));
// 使用目标类型为Comparator的Lambda表达式对List集合排序
books.sort((o1, o2)->((String)o1).length() - ((String)o2).length());
System.out.println(books);
// 使用目标类型为UnaryOperator的Lambda表达式来替换集合中所有元素
// 该Lambda表达式控制使用每个字符串的长度作为新的集合元素
books.replaceAll(ele->((String)ele).length());
System.out.println(books); // 输出[7, 8, 11, 16]
}
}
注意(待调研)
replaceAll该方法是List接口提供的,在ArrayList中重写的,这里的ArrayList表示的是实现List接口的实现类。
使用Arrays.asList()方法返回的ArrayList是Arrays内部提供的ArrayList静态内部类,该ArrayList和上述ArrayList不是同一个ArrayList,因此,方法不能互用。
迭代器
List额外提供了一个listlterator()方法,该方法返回一个Listlterator对象,Listlterator 接口继承了lterator接口,提供了专门操作List 的方法。Listlterator接口在lterator接口基础上增加了如下方法。
- boolean hasPrevious():返回该迭代器关联的集合是否还有上一个元素。
- Object previous():返回该迭代器的上一个元素。
- void add(Object o):在指定位置插入一个元素。
拿ListIterator 与普通的Iterator进行对比,不难发现Listlterator增加了向前迭代的功能(Iterator 只能向后迭代),而且ListIlterator还可通过add()方法向List集合中添加元素(Iterator只能删除元素)。下面程序示范了ListIterator的用法。
public class ListIteratorTest
{
public static void main(String[] args)
{
String[] books = {
"疯狂Java讲义", "疯狂iOS讲义",
"轻量级Java EE企业应用实战"
};
List bookList = new ArrayList();
for (int i = 0; i < books.length ; i++ )
{
bookList.add(books[i]);
}
ListIterator lit = bookList.listIterator();
while (lit.hasNext())
{
System.out.println(lit.next());
// lit.add("-------分隔符-------");
}
System.out.println("=======下面开始反向迭代=======");
while(lit.hasPrevious())
{
System.out.println(lit.previous());
}
}
}
ArrayList和Vector实现类
-
ArrayList和 Vector作为List类的两个典型实现,完全支持前面介绍的List接口的全部功能。
-
ArrayList和 Vector类都是基于数组实现的List类,ArrayList和Vector类封装了一个动态的、允许再分配的Object[]数组。Object[]数组的长度默认为10。
-
ArrayList或Vector对象使用initialCapacity参数来设置该数组的长度,当向ArrayList或 Vector中添加元素超出了该数组的长度时,它们的initialCapacity 会自动增加。
-
如果向ArrayList或Vector集合中添加大量元素时,可使用ensureCapacity(int minCapacity)方法一次性地增加initialCapacity。这可以减少重分配的次数,从而提高性能。
-
如果开始就知道ArrayList或 Vector集合需要保存多少个元素,则可以在创建它们时就指定initialCapacity大小。
-
List<String> a = new ArrayList<>(15);
-
-
ArrayList和Vector在用法上几乎完全相同,Vector比较老,基本弃用,但是Vector线程是安全的,ArrayList是不安全的,但是实际使用中,需要线程的场景也不实用Vector,而是使用Collections.synchronizedList ()。
-
//线程不安全: List<Map<String,Object>> data=new ArrayList<Map<String,Object>>(); //线程安全: List<Map<String,Object>> data=Collections.synchronizedList (new ArrayList<Map<String,Object>> ())
-
-
Vector提供了一个子类Stack,栈,先进后出,比较老,不使用。通常使用ArrayDeque代替。
- ArrayDeque底层基于数组,实现了List和Deque接口,性能好。
ArrayList和 Vector还提供了如下两个方法来重新分配Object[]数组。
- void ensureCapacity(int minCapacity):将ArrayList或Vector集合的Object[]数组长度增加大于或等于minCapacity值。
- void trimToSize():调整ArrayList或Vector集合的Object[]数组长度为当前元素的个数。调用该方法可减少ArrayList或 Vector集合对象占用的存储空间。
ArrayList b = new ArrayList(10);
b.add(1);
b.add(1);
b.add(1);
b.add(1);
b.add(1);
b.ensureCapacity(4);
b.trimToSize();
System.out.println(b.size());
固定长度的List
前面讲数组时介绍了一个操作数组的工具类: Arrays,该工具类里提供了asList(Object… a)方法,该方法可以把一个数组或指定个数的对象转换成一个List集合,这个 List集合既不是ArrayList 实现类的实例,也不是 Vector实现类的实例,而是Arrays的内部类ArrayList的实例。
Arrays.ArrayList是一个固定长度的List集合,程序只能遍历访问该集合里的元素,不可增加、删除该集合里的元素。
原因:里面存储数据的数组是使用final修饰的。
public class FixedSizeList
{
public static void main(String[] args)
{
List fixedList = Arrays.asList("疯狂Java讲义"
, "轻量级Java EE企业应用实战");
// 获取fixedList的实现类,将输出Arrays$ArrayList 表名他是一个内部类。
System.out.println(fixedList.getClass());
// 使用方法引用遍历集合元素
fixedList.forEach(System.out::println);
// 试图增加、删除元素都会引发UnsupportedOperationException异常
fixedList.add("疯狂Android讲义");
fixedList.remove("疯狂Java讲义");
}
}
3. Queue接口(了解,待深入)
Queue用于模拟队列这种数据结构,队列通常是指“先进先出”(FIFO)的容器。
新元素插入(offer)到队列的尾部,访问元素(poll)操作会返回队列头部的元素。
通常,队列不允许随机访问队列中的元素。
Queue接口中定义了如下几个方法。
- void add(Object e):将指定元素加入此队列的尾部。
- Object element():获取队列头部的元素,但是不删除该元素。
- boolean offer(Object e):将指定元素加入此队列的尾部。当使用有容量限制的队列时,此方法通常比 add(Object e)方法更好。
- Object peek():获取队列头部的元素,但是不删除该元素。如果此队列为空,则返回null。
- Object poll():获取队列头部的元素,并**删除该元素。**如果此队列为空,则返回null。
- Object remove():获取队列头部的元素,并删除该元素。
Queue接口有一个PriorityQueue实现类。
Queue还有一个Deque接口,Deque代表一个“双端队列”,双端队列可以同时从两端来添加、删除元素,因此 Deque 的实现类既可当成队列使用,也可当成栈使用。
Java为 Deque提供了ArrayDeque和 LinkedList两个实现类。
PriorityQueue(优先队列)实现类
推荐Blog
-
https://www.cnblogs.com/guanghe/p/13450971.html
-
https://blog.csdn.net/ohwang/article/details/116934308
-
PriorityQueue不是绝对标准的队列,PriorityQueue保存队列元素的顺序并不是按加入队列的顺序,而是按队列元素的大小进行重新排序。
-
当调用peek()方法或者poll()方法取出队列中的元素时,并不是取出最先进入队列的元素,而是取出队列中最小的元素。违反了队列先进先出(FIFO)的基本规则。
普通使用
public class PriorityQueueTest
{
public static void main(String[] args)
{
PriorityQueue pq = new PriorityQueue();
// 下面代码依次向pq中加入四个元素
pq.offer(6);
pq.offer(-3);
pq.offer(20);
pq.offer(18);
// 输出pq队列,并不是按元素的加入顺序排列
System.out.println(pq); // 输出[-3, 6, 20, 18]
// 访问队列第一个元素,其实就是队列中最小的元素:-3
System.out.println(pq.poll());
}
}
问题:
- 直接输出集合,发现并没有按照大小排序,但是取出集合中的元素时,是从小到大取出的。
- 了解优先队列的底层。
PriorityQueue的元素有两种排序方式。
自然排序:采用自然顺序的 PriorityQueue集合中的元素必须实现了Comparable接口,而且应该是同一个类的多个实例,否则可能导致ClassCastException异常。
定制排序:创建PriorityQueue队列时,传入一个Comparator对象,该对象负责对队列中的所有元素进行排序。采用定制排序时不要求队列元素实现 Comparable接口。
使用Comparator
创建Employee类
public class Employee {
private Long id;
private String name;
private LocalDate dob;
public Employee(Long id, String name, LocalDate dob) {
super();
this.id = id;
this.name = name;
this.dob = dob;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
@Override
public String toString() {
return "Employee [id=" + id + ", name=" + name + ", dob=" + dob + "]";
}
public static void main(String[] args) {
Comparator<Employee> nameSorter = Comparator.comparing(Employee::getId);
PriorityQueue<Employee> priorityQueue = new PriorityQueue<>(nameSorter);
priorityQueue.add(new Employee(1l, "AAA", LocalDate.now()));
priorityQueue.add(new Employee(4l, "CCC", LocalDate.now()));
priorityQueue.add(new Employee(5l, "BBB", LocalDate.now()));
priorityQueue.add(new Employee(2l, "FFF", LocalDate.now()));
priorityQueue.add(new Employee(3l, "DDD", LocalDate.now()));
priorityQueue.add(new Employee(6l, "EEE", LocalDate.now()));
while(true) {
Employee e = priorityQueue.poll();
System.out.println(e);
if(e == null) break;
}
}
}
使用Comparable
public class Employees implements Comparable<Employees> {
private Long id;
private String name;
private LocalDate dob;
public Long getId() {
return id;
}
public Employees(Long id, String name, LocalDate dob) {
super();
this.id = id;
this.name = name;
this.dob = dob;
}
@Override
public int compareTo(Employees emp) {
return this.getId().compareTo(emp.getId());
}
//Getters and setters
@Override
public String toString() {
return "Employee [id=" + id + ", name=" + name + ", dob=" + dob + "]";
}
public static void main(String[] args) {
PriorityQueue<Employees> priorityQueue = new PriorityQueue<>();
priorityQueue.add(new Employees(1l, "AAA", LocalDate.now()));
priorityQueue.add(new Employees(4l, "CCC", LocalDate.now()));
priorityQueue.add(new Employees(5l, "BBB", LocalDate.now()));
priorityQueue.add(new Employees(2l, "FFF", LocalDate.now()));
priorityQueue.add(new Employees(3l, "DDD", LocalDate.now()));
priorityQueue.add(new Employees(6l, "EEE", LocalDate.now()));
while(true) {
Employees e = priorityQueue.poll();
System.out.println(e);
if(e == null) break;
}
}
}
练习
使用Comparator处理Integer
因为Integer,Double等Java包装类的比较规则Java已经默认提供,因此我们可以不提供相关比较接口。
public static void main(String[] args) {
//方法一: Queue priorityQueue = new PriorityQueue((v1,v2)->((Integer) v2)-((Integer) v1));
//方法二:
// Queue priorityQueue = new PriorityQueue(Comparator.comparing(Integer::intValue));
//方法三:
Queue priorityQueue = new PriorityQueue();
priorityQueue.offer(1);
priorityQueue.offer(5);
priorityQueue.offer(3);
System.out.println(priorityQueue);
System.out.println(priorityQueue.poll());
System.out.println(priorityQueue.poll());
System.out.println(priorityQueue.poll());
}
使用Comparator处理自定义类
package lession11_29;
import java.util.Comparator;
import java.util.PriorityQueue;
import java.util.Queue;
/**
* @Classname TestQueue
* @Description TODO
* @Date 2021/11/29 9:03
* @Created by DELL
*/
class Person {
private int age = 18;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"age=" + age +
'}';
}
}
public class TestQueue {
public static void main(String[] args) {
Person a = new Person();
a.setAge(18);
Person c = new Person();
c.setAge(20);
Person b = new Person();
b.setAge(19);
//用法一:comparator
//Comparator comparator = Comparator.comparing(Person::getAge);
//PriorityQueue<Person> priorityQueue = new PriorityQueue<>(comparator);
//用法二:
//PriorityQueue<Person> priorityQueue = new PriorityQueue<>(Comparator.comparing(Person::getAge));
//用法三:
Queue priorityQueue = new PriorityQueue(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
Person person1 = (Person) o1;
Person person2 = (Person) o2;
return person1.getAge()<person2.getAge()
? -1:person1.getAge()>person2.getAge()
?1:0;
}
});
priorityQueue.offer(a);
priorityQueue.offer(b);
priorityQueue.offer(c);
System.out.println(priorityQueue);
System.out.println(priorityQueue.poll());
System.out.println(priorityQueue.poll());
System.out.println(priorityQueue.poll());
}
}
使用Comparable处理自定义类
注意:自定义类不实现Comparable接口且不提供Comparator会报错。原因:程序不知道如何比较插入元素的大小。
package lession11_29;
import java.util.Comparator;
import java.util.PriorityQueue;
import java.util.Queue;
/**
* @Classname TestQueue
* @Description TODO
* @Date 2021/11/29 9:03
* @Created by DELL
*/
class Person implements Comparable {
private int age = 18;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"age=" + age +
'}';
}
@Override
public int compareTo(Object o) {
Person person = (Person) o;
return this.getAge() > person.getAge() ? -1 :
this.getAge() < person.getAge()
? 1 : 0;
}
}
public class TestQueue {
public static void main(String[] args) {
Person a = new Person();
a.setAge(18);
Person c = new Person();
c.setAge(20);
Person b = new Person();
b.setAge(19);
//实现Comparable接口,往优先级队列中插入元素。
Queue priorityQueue = new PriorityQueue();
priorityQueue.offer(a);
priorityQueue.offer(b);
priorityQueue.offer(c);
System.out.println(priorityQueue);
System.out.println(priorityQueue.poll());
System.out.println(priorityQueue.poll());
System.out.println(priorityQueue.poll());
}
}
Deque口与ArrayDeque 实现类
Deque接口是Queue接口的子接口,它代表一个双端队列,Deque接口里定义了一些双端队列的方法,这些方法允许从两端来操作队列的元素。
Deque接口的实现类: ArrayDeque,一个基于数组实现的双端队列,创建Deque时同样可指定一个numElements参数,该参数用于指定Object[]数组的长度;如果不指定numElements参数,Deque底层数组的长度为16。
- ArrayDeque可以作为栈使用,来代替Stack(老,性能差劲)
- ArrayDeque可以作为队列使用
ArrayList和ArrayDeque两个集合类的实现机制基本相似,底层都采用一个动态的、可重分配的Object[]数组来存储集合元素,当集合元素超出了该数组的容量时,系统会在底层重新分配一个Object[]数组来存储集合元素。
- void addFirst(Object e):将指定元素插入该双端队列的开头。
- void addLast(Object e):将指定元素插入该双端队列的末尾。
- lterator descendingIterator():返回该双端队列对应的迭代器,该迭代器将以逆向顺序来迭代队列中的元素。
- Object getFirst():获取但不删除双端队列的第一个元素。Object getLast():获取但不删除双端队列的最后一个元素。
- boolean offerFirst(Object e):将指定元素插入该双端队列的开头。
- boolean offerLast(Object e):将指定元素插入该双端队列的末尾。
- Object peekFirst():获取但不删除该双端队列的第一个元素;如果此双端队列为空,则返回null。
- ObjectpeekLast():获取但不删除该双端队列的最后一个元素;如果此双端队列为空,则返回null。
- Object pollFirst():获取并删除该双端队列的第一个元素;如果此双端队列为空,则返回null。
- Object pollLast():获取并删除该双端队列的最后一个元素;如果此双端队列为空,则返回null。
- Object pop()(栈方法): pop出该双端队列所表示的栈的栈顶元素。相当于removeFirst()。
- void push(Object e)(栈方法):将一个元素 push进该双端队列所表示的栈的栈顶。相当于addFirst(e)。
- Object removeFirst():获取并删除该双端队列的第一个元素。
- Object removeFirstOccurrence(Object o):删除该双端队列的第一次出现的元素o。
- Object removeLast():获取并删除该双端队列的最后一个元素。
- boolean removeLastOccurrence(Object o):删除该双端队列的最后一次出现的元素o。
从上面方法中可以看出,Deque不仅可以当成双端队列使用,而且可以被当成栈来使用,包含了pop(出栈)、push(入栈)两个方法。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-f5pT2afx-1638542005483)(C:\Users\DELL\AppData\Roaming\Typora\typora-user-images\image-20211125225508637.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GeKHDLvz-1638542005483)(C:\Users\DELL\AppData\Roaming\Typora\typora-user-images\image-20211125225528886.png)]
ArrayDeque的栈使用
public class ArrayDequeStack
{
public static void main(String[] args)
{
ArrayDeque stack = new ArrayDeque();
// 依次将三个元素push入"栈"
stack.push("疯狂Java讲义");
stack.push("轻量级Java EE企业应用实战");
stack.push("疯狂Android讲义");
// 输出:[疯狂Android讲义, 轻量级Java EE企业应用实战, 疯狂Java讲义]
System.out.println(stack);
// 访问第一个元素,但并不将其pop出"栈",输出:疯狂Android讲义
System.out.println(stack.peek());
// 依然输出:[疯狂Android讲义, 疯狂Java讲义, 轻量级Java EE企业应用实战]
System.out.println(stack);
// pop出第一个元素,输出:疯狂Android讲义
System.out.println(stack.pop());
// 输出:[轻量级Java EE企业应用实战, 疯狂Java讲义]
System.out.println(stack);
}
}
ArrayDeque的队列使用
public class ArrayDequeQueue
{
public static void main(String[] args)
{
ArrayDeque queue = new ArrayDeque();
// 依次将三个元素加入队列
queue.offer("疯狂Java讲义");
queue.offer("轻量级Java EE企业应用实战");
queue.offer("疯狂Android讲义");
// 输出:[疯狂Java讲义, 轻量级Java EE企业应用实战, 疯狂Android讲义]
System.out.println(queue);
// 访问队列头部的元素,但并不将其poll出队列"栈",输出:疯狂Java讲义
System.out.println(queue.peek());
// 依然输出:[疯狂Java讲义, 轻量级Java EE企业应用实战, 疯狂Android讲义]
System.out.println(queue);
// poll出第一个元素,输出:疯狂Java讲义
System.out.println(queue.poll());
// 输出:[轻量级Java EE企业应用实战, 疯狂Android讲义]
System.out.println(queue);
}
}
LinkedList实现类
即实现了List接口,又实现了Deque接口,可以通过索引来随机访问集合中的元素,也可以被当成栈或者队列使用。
public class LinkedListTest
{
public static void main(String[] args)
{
LinkedList books = new LinkedList();
// 将字符串元素加入队列的尾部
books.offer("疯狂Java讲义");
// 将一个字符串元素加入栈的顶部
books.push("轻量级Java EE企业应用实战");
// 将字符串元素添加到队列的头部(相当于栈的顶部)
books.offerFirst("疯狂Android讲义");
// 以List的方式(按索引访问的方式)来遍历集合元素
for (int i = 0; i < books.size() ; i++ )
{
System.out.println("遍历中:" + books.get(i));
}
// 访问、并不删除栈顶的元素
System.out.println(books.peekFirst());
// 访问、并不删除队列的最后一个元素
System.out.println(books.peekLast());
// 将栈顶的元素弹出“栈”
System.out.println(books.pop());
// 下面输出将看到队列中第一个元素被删除
System.out.println(books);
// 访问、并删除队列的最后一个元素
System.out.println(books.pollLast());
// 下面输出:[轻量级Java EE企业应用实战]
System.out.println(books);
}
}
List和Queue的实现类对比
基于数组实现的集合:如:ArrayList,ArrayDeque
- 随机访问集合性能较好,但是不适用频繁插入操作。
- Vector(线程安全的)使用ArrayList代替,线程同步时,使用Collections.synchronizedList (new ArrayList<> ())
基于链表实现的集合:如:LinkedList
- 使用迭代器进行集合的遍历,适用于频繁的插入,删除场景。正常使用的是ArrayList
4. Set集合
Set集合是无序的,Set不允许包含重复元素。与Collection相同,没有提供新的方法。
HashSet实现类
HashSet是 Set接口的典型实现,按 Hash算法来存储集合中的元素,具有很好的存取和查找性能,大多数时候使用Set集合时使用这个实现类。
特点:
- 不能保证元素的排列顺序,顺序可能与添加顺序不同。
- HashSet 不是同步的,如果多个线程同时访问一个HashSet,假设有两个或者两个以上线程同时
修改了HashSet集合时,则必须通过代码来保证其同步。 - 集合元素值可以是null。
注意:
set集合中的元素,原则上不能删除和修改,只做重复数据的删选。
介绍:
当向 HashSet集合中存入一个元素时,HashSet会调用该对象的 hashCode()方法来得到该对象的hashCode值,然后根据该hashCode值决定该对象在HashSet中的存储位置。如果有两个元素通过equals()方法比较返回true,但它们的hashCode()方法返回值不相等,HashSet 将会把它们存储在不同的位置,依然可以添加成功。
HashSet集合判断两个元素相等的标准是两个对象通过 equals()方法比较相等,并且两个对象的 hashCode()方法返回值也相等。
HashSet元素比较的源码:
重写equals和hashCode方法
注意:
- 将对象放入HashSet方法时,需要重写equals方法和hashcode方法,重写原则:equals返回true,hashcode为true。
- 如果两个对象equals相等,但是hashcode不相等,这两个对象会存在Hash表不同的位置,并且都可以添加成功,违背了set集合的原则
- 如果两个对象hashcode相等,但是equals不相等,那么这两个对象会存在同一hash表的位置,会导致性能下降。
- 附:两个对象的hashcode相等,两个对象不一定相等。相等的两个对象的hashcode值一定相等。
// 类A的equals方法总是返回true,但没有重写其hashCode()方法
class A
{
public boolean equals(Object obj)
{
return true;
}
}
// 类B的hashCode()方法总是返回1,但没有重写其equals()方法
class B
{
public int hashCode()
{
return 1;
}
}
// 类C的hashCode()方法总是返回2,且重写其equals()方法总是返回true
class C
{
public int hashCode()
{
return 2;
}
public boolean equals(Object obj)
{
return true;
}
}
public class HashSetTest
{
public static void main(String[] args)
{
HashSet books = new HashSet();
// 分别向books集合中添加两个A对象,两个B对象,两个C对象
books.add(new A());
books.add(new A());
books.add(new B());
books.add(new B());
books.add(new C());
books.add(new C());
System.out.println(books);
}
}
HashCode的重写原则
(87条消息) hashCode()剖析_zhang19903848257的博客-CSDN博客
重写hashCode()规则
使用对象内有意义的属性值,计算成hashcode,再将在这有意义的hashcode进行组合后,返回一个int类型的数值。
- 在程序运行过程中,同一个对象多次调用hashCode()方法应该返回相同的值。
- 当两个对象通过equals()方法比较返回true时,这两个对象的 hashCode()方法应返回相等的值。
- 对象中用作equals()方法比较标准的实例变量,都应该用于计算 hashCode值。
添加可变对象
如果向HashSet 中添加一个可变对象后,后面程序修改了该可变对象的实例变量,则可能导致它与集合中的其他元素相同(即两个对象通过equals()方法比较返回true,两个对象的 hashCode值也相等),这就有可能导致HashSet中包含两个相同的对象。
class R
{
int count;
public R(int count)
{
this.count = count;
}
public String toString()
{
return "R[count:" + count + "]";
}
public boolean equals(Object obj)
{
if(this == obj)
return true;
if (obj != null && obj.getClass() == R.class)
{
R r = (R)obj;
return this.count == r.count;
}
return false;
}
public int hashCode()
{
return this.count;
}
}
public class HashSetTest2
{
public static void main(String[] args)
{
HashSet hs = new HashSet();
hs.add(new R(5));
hs.add(new R(-3));
hs.add(new R(9));
hs.add(new R(-2));
// 打印HashSet集合,集合元素没有重复
System.out.println(hs);
// 取出第一个元素
Iterator it = hs.iterator();
R first = (R)it.next();
// 为第一个元素的count实例变量赋值
first.count = -3;
// 再次输出HashSet集合,集合元素有重复元素
System.out.println(hs);
// 删除count为-3的R对象
hs.remove(new R(-3)); //删除的是hashcode和equals都符合的元素,应该是集合中的第二个元素
// 可以看到被删除了一个R元素
System.out.println(hs);
System.out.println("hs是否包含count为-3的R对象?"
+ hs.contains(new R(-3))); // 输出false,因为hashcode和equals都符合的元素已经被删除。
System.out.println("hs是否包含count为-2的R对象?"
+ hs.contains(new R(-2))); // 输出false 因为hashcode符合元素,equals却不符合。
}
}
-
hs.remove(new R(-3));
- 删除的是hashcode和equals都符合的元素,应该是集合中的第二个元素
-
System.out.println(“hs是否包含count为-3的R对象?”+ hs.contains(new R(-3)));
- 输出false,因为hashcode和equals都符合的元素已经被删除。
-
System.out.println("hs是否包含count为-2的R对象?"hs.contains(new R(-2)));
- 输出false 因为hashcode符合元素,equals却不符合。
最后:因为我们改变了HashSet集合中的元素的属性值,导致进行集合中的元素hashcode的比较和对象属性值的比较不一样,导致HashSet无法正确操作这些元素。
Set的使用
public class Student {
private Integer idcard;
private String name;
private String school;
public Student(Integer idcard, String name, String school) {
this.idcard = idcard;
this.name = name;
this.school = school;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return idcard != null ? idcard.equals(student.idcard) : student.idcard == null;
}
@Override
public int hashCode() {
return idcard != null ? idcard.hashCode() : 0;
}
@Override
public String toString() {
return "Student{" +
"idcard=" + idcard +
", name='" + name + '\'' +
", school='" + school + '\'' +
'}';
}
public Integer getIdcard() {
return idcard;
}
public void setIdcard(Integer idcard) {
this.idcard = idcard;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSchool() {
return school;
}
public void setSchool(String school) {
this.school = school;
}
public static void main(String[] args) {
Student student1 = new Student(1, "张1", "河师大");
Student student2 = new Student(2, "张2", "河师大");
Student student3 = new Student(3, "张3", "新乡医学院");
Student student4 = new Student(4, "张1", "新乡医学院");
Student student5 = new Student(5, "张2", "河南工程学院");
Student student6 = new Student(6, "张3", "河南工程学院");
Student student7 = new Student(7, "张2", "新乡学院");
Student student8 = new Student(8, "张3", "新乡学院");
List<Student> stuList = new ArrayList<>();
stuList.add(student1);
stuList.add(student2);
stuList.add(student3);
stuList.add(student4);
stuList.add(student5);
stuList.add(student6);
stuList.add(student7);
stuList.add(student8);
List<String> schoolList = stuList.stream().map(Student::getSchool).collect(Collectors.toList());
System.out.println(schoolList);
Set<String> schoolSet = stuList.stream().map(Student::getSchool).collect(Collectors.toSet());
System.out.println(schoolSet);
}
}
LinkedHashSet实现类
HashSet的子类LinkedHashSet,LinkedHashSet集合也是根据元素的 hashCode值来决定元素的存储位置,同时使用链表维护元素的次序,这样使得元素看起来是以插入的顺序保存的。当遍历LinkedHashSet集合里的元素时,LinkedHashSet将会按元素的添加顺序来访问集合里的元素。注意:HashSet子类,同样不能添加重复元素。
LinkedHashSet需要维护元素的插入顺序,因此性能略低于HashSet的性能,但在迭代访问Set里的全部元素时将有很好的性能,因为它以链表来维护内部顺序。
public class LinkedHashSetTest
{
public static void main(String[] args)
{
LinkedHashSet books = new LinkedHashSet();
books.add("疯狂Java讲义");
books.add("轻量级Java EE企业应用实战");
System.out.println(books);
// 删除 疯狂Java讲义
books.remove("疯狂Java讲义");
// 重新添加 疯狂Java讲义
books.add("疯狂Java讲义");
System.out.println(books);
}
}
运行结果:
[疯狂Java讲义, 轻量级Java EE企业应用实战]
[轻量级Java EE企业应用实战, 疯狂Java讲义]
TreeSet实现类
TreeSet是SortedSet接口的实现类,TreeSet可以确保集合元素处于排序状态(以大小进行排序,并不是插入顺序)。与HashSet集合相比,TreeSet提供了如下几个额外的方法。
注意:
往TreeSet集合中添加元素时,通过compareTo方法的返回值是否为0来判断是否为相同的元素。如果为0,表示集合中已存在该元素,不能添加。
定制规则以及获取前一个,后一个元素
- Comparator comparator():如果TreeSet采用了定制排序,则该方法返回定制排序所使用的Comparator;如果TreeSet采用了自然排序,则返回null。
- Object first():返回集合中的第一个元素。
- Object last():返回集合中的最后一个元素。
- Object lower(Object e):返回集合中位于指定元素之前的元素(即小于指定元素的最大元素,参考元素不需要是TreeSet集合里的元素)。
- Object higher (Object e):返回集合中位于指定元素之后的元素(即大于指定元素的最小元素,参考元素不需要是TreeSet集合里的元素)。
获取子集
- SortedSet subSet(Object fromElement, Object toElement):返回此Set的子集合,范围从fromElement(包含)到toElement(不包含)。
- SortedSet headSet(Object toElement):返回此Set的子集,由小于toElement的元素组成。
- SortedSet tailSet(Object fromElement):返回此Set 的子集,由大于或等fromElement 的元素组成
class M
{
int age;
public M(int age)
{
this.age = age;
}
public String toString()
{
return "M [age:" + age + "]";
}
}
public class TreeSetTest4
{
public static void main(String[] args)
{
// 此处Lambda表达式的目标类型是Comparator
TreeSet ts = new TreeSet((o1 , o2) ->
{
M m1 = (M)o1;
M m2 = (M)o2;
// 根据M对象的age属性来决定大小,age越大,M对象反而越小
return m1.age > m2.age ? -1
: m1.age < m2.age ? 1 : 0;
});
ts.add(new M(5));
ts.add(new M(-3));
ts.add(new M(9));
System.out.println(ts);
}
}
TreeSet的存储和排序
HashSet是采用Hash算法来决定元素存储位置的不同。
TreeSet采用红黑树的数据结构来存储集合元素。
TreeSet支持自然排序和定制排序两种方式。
自然排序
TreeSet 会调用集合元素的compareTo(Object obj)方法来比较元素之间的大小关系,然后将集合元素按升序排列,这种方式就是自然排序。当compareTo返回0时,则认为他们相等,不让元素进入集合。
compareTo方法是Comparable接口提供的一个方法,使用自然排序时,实体类必须要实现Comparable接口。否则往集合中添加元素时,会出现报错。
如果试图把一个对象添加到TreeSet时,则该对象的类必须实Comparable接口,否则程序将会抛出异常。如下程序示范了这个错误。
不实现Comparable接口
class M
{
int age;
public M(int age)
{
this.age = age;
}
public String toString()
{
return "M [age:" + age + "]";
}
}
public class TreeSetTest4
{
public static void main(String[] args)
{
// 此处Lambda表达式的目标类型是Comparator
TreeSet ts = new TreeSet();
ts.add(new M(5));//运行没有问题
ts.add(new M(-3));//产生异常。
ts.add(new M(9));
System.out.println(ts);
}
}
注意:往集合中添加-3时,报错,因为此时集合中有元素,会调用Compare方法,但是没有实现Comparable接口,没有Compare方法。
添加第一个元素时,不进行比较,因此没有报错。
添加字符串和其他类型对象
public class TreeSetTest4 {
public static void main(String[] args) {
// 此处Lambda表达式的目标类型是Comparator
TreeSet ts = new TreeSet();
ts.add(new String("123456"));
ts.add(new Date());
}
}
报错:
字符串的Comparable接口已经实现,要求比较的是String类型的数据,而不是Object类型
在自定义实现Comparable接口的情况下(compareTo(Object obj)的实现没有强制类型转换),TreeSet支持添加多种类型的自定义类型对象,但是迭代集合时,不同类型的元素类型会发生ClassCastException异常。
判断添加元素是否相等
通过compareTo方法,来判断返回值,返回值为0,则认为相等。
class Z implements Comparable
{
int age;
public Z(int age)
{
this.age = age;
}
// 重写equals()方法,总是返回true
public boolean equals(Object obj)
{
return true;
}
// 重写了compareTo(Object obj)方法,总是返回1,即不相等
public int compareTo(Object obj)
{
return 1;
}
public int getAge() {
return age;
}
}
public class TreeSetTest2
{
public static void main(String[] args)
{
TreeSet set = new TreeSet();
Z z1 = new Z(6);
set.add(z1);
// 第二次添加同一个对象,输出true,表明添加成功
System.out.println(set.add(z1));
// 下面输出set集合,将看到有两个元素
Iterator iterator = set.stream().iterator();
int a =1;
while (iterator.hasNext()){
Z next =(Z) iterator.next();
System.out.println("元素"+a+":"+next.getAge());
a++;
}
// 修改set集合的第一个元素的age变量
((Z)(set.first())).age = 9;
// 输出set集合的最后一个元素的age变量,将看到也变成了9
System.out.println(((Z)(set.last())).age);
}
}
我们向集合中添加了两个一样的对象,虽然equals方法返回true,但是compareTo返回的是1,因此集合认为他们不是同一个对象,因此可以正常添加。但是当compareTo返回0,equals返回false的时候,是可以添加进去的。
至于为什么两个元素同时修改,是因为我们两次添加的是对象z1,是同一个对象。
注意:
Comparter比较只关心compareTo的返回值,0则相同,1则不相同,与equals方法无关
规则:
- compareTo1,equals true 判断不是同一个对象,可以添加
- compareTo0, equals false 判断是同一个对象,不可以添加
- 使用自定义对象时,我们要保证,重写的compareTo和equals方法的返回效果要一致。
修改TreeSet中的对象属性
如果向TreeSet 中添加一个可变对象后,并且后面程序修改了该可变对象的实例变量,这将导致它与其他对象的大小顺序发生了改变,但TreeSet 不会再次调整它们的顺序,甚至可能导致TreeSet 中保存的这两个对象通过compareTo(Object obj)方法比较返回0。
class S implements Comparable
{
int count;
public S(int count)
{
this.count = count;
}
public String toString()
{
return "S[count:" + count + "]";
}
// 重写equals方法,根据count来判断是否相等
public boolean equals(Object obj)
{
if (this == obj)
{
return true;
}
if(obj != null && obj.getClass() == R.class)
{
S r = (S)obj;
return r.count == this.count;
}
return false;
}
// 重写compareTo方法,根据count来比较大小
public int compareTo(Object obj)
{
S r = (S)obj;
return count > r.count ? 1 :
count < r.count ? -1 : 0;
}
}
public class TreeSetTest3
{
public static void main(String[] args)
{
TreeSet ts = new TreeSet();
ts.add(new S(5));
ts.add(new S(-3));
ts.add(new S(9));
ts.add(new S(-2));
// 打印TreeSet集合,集合元素是有序排列的
System.out.println(ts); // ①
// 取出第一个元素
S first = (S)ts.first();
// 对第一个元素的count赋值
first.count = 20;
// 取出最后一个元素
S last = (S)ts.last();
// 对最后一个元素的count赋值,与第二个元素的count相同
last.count = -2;
// 再次输出将看到TreeSet里的元素处于无序状态,且有重复元素
System.out.println(ts);
// 删除实例变量被改变的元素,删除失败
System.out.println(ts.remove(new S(-2))); // ③
System.out.println(ts);
// 删除实例变量没有被改变的元素,删除成功
System.out.println(ts.remove(new S(5))); //
System.out.println(ts);
System.out.println(ts.remove(new S(20))); //删除失败 因为该元素修改过
}
}
结果:
[S[count:-3], S[count:-2], S[count:5], S[count:9]] // 有序
[S[count:20], S[count:-2], S[count:5], S[count:-2]] // 修改后,编程了无序
false //删除失败,修改后的元素与原集合中的元素重复,无法判断要删除那个元素
[S[count:20], S[count:-2], S[count:5], S[count:-2]]
true //删除成功
[S[count:20], S[count:-2], S[count:-2]]
注意:
- 修改了Tree集合中的值后,可能会造成与集合中的其他值一样,测试集合不会去重,并且不会重新排序
- 修改后,如果和集合中其他元素的值重复,是无法进行删除的
- Tree只能删除没有修改且没有没有集合中没有重复的对象
- 附:测试过程中发现的问题
- 在 上述代码最后 System.out.println(ts.remove(new S(20))); 的这个操作,删除失败。
- 但是删除-2的操作,可以成功(集合中是有两个-2的),当我再次删除-2时,还是可以删除的。我此时删除20,也是可以删除的。
- 看资料,说的是删除成功后,发生了元素索引的重排(之后就可以排序了),但是出现了上述1,2的情况,总之往set里面存储元素时,不要修改元素的属性值。
总之:往Set集合中添加元素时,不要修改元素的属性值。HashSet会造成地址值一样,而Tree可能造成compareTo返回值一样
定制排序
TreeSet的自然排序是根据集合元素的大小,将升序排列。如果需要实现定制排序,例如以降序排列,则可以通过Comparator接口。
该接口里包含一个int compare(T ol,T o2)方法,该方法用于比较ol和o2的大小:如果该方法返回正整数,则表明o1大于o2;如果该方法返回0,则表明ol等于o2;如果该方法返回负整数,则表明o1小于o2。
实现定制排序,则需要在创建TreeSet集合对象时,提供一个Comparator 对象与该TreeSet集合关联,由该Comparator对象负责集合元素的排序逻辑。Comparator是一个函数式接口,可使用Lambda表达式来代替Comparator对象。
class M
{
int age;
public M(int age)
{
this.age = age;
}
public String toString()
{
return "M [age:" + age + "]";
}
}
public class TreeSetTest4
{
public static void main(String[] args)
{
// 此处Lambda表达式的目标类型是Comparator
TreeSet ts = new TreeSet((o1 , o2) ->
{
M m1 = (M)o1;
M m2 = (M)o2;
// 根据M对象的age属性来决定大小,age越大,M对象反而越小
return m1.age > m2.age ? -1
: m1.age < m2.age ? 1 : 0;
});
ts.add(new M(5));
ts.add(new M(-3));
ts.add(new M(9));
System.out.println(ts);
}
}
9 5 -3
注意:
当通过Comparator对象(或Lambda表达式)来实现TreeSet 的定制排序时,依然不可以向TreeSet中添加类型不同的对象,否则会引发ClassCastException 异常。使用定制排序时,TreeSet对集合元素排序不管集合元素本身的大小,而是由Comparator对象(或Lambda表达式)负责集合元素的排序规则。TreeSet判断两个集合元素相等的标准是:通过Comparator (或Lambda表达式)比较两个元素返回了0,这样TreeSet 不会把第二个元素添加到集合中。
EnumSet类
EnumSet是为枚举类设计的集合类,EnumSet中的所有元素都必须是指定枚举类型的枚举值,该枚举类型在创建EnumSet时显式或隐式地指定。
特征:
- 所有元素必须枚举类型,否则报错
- 有序排列,按照枚举在枚举类中定义的顺序
- EnumSet在内部以位向量的形式存储,这种存储形式非常紧凑、高效,因此EnumSet对象占用内存很小,而且运行效率很好。尤其是进行批量操作(如调用containsAll() 和retainAll()方法)时,如果其参数也是 EnumSet集合,则该批量操作的执行速度也非常快。
- 不允许加入null元素,添加会报错。判断是否包含null元素不会报异常,删除空的元素会返回false
构造器
EnumSet类没有暴露任何构造器来创建该类的实例,程序应该通过它提供的类方法来创建EnumSet对象。
- EnumSet allOf(Class elementType):创建一个包含指定枚举类里所有枚举值的EnumSet集合。
- EnumSet complementOf(EnumSet s):创建一个其元素类型与指定EnumSet里元素类型相同的EnumSet集合,新EnumSet集合包含原EnumSet集合所不包含的、此枚举类剩下的枚举值(即新EnumSet集合和原EnumSet集合的集合元素加起来就是该枚举类的所有枚举值)。
- EnumSet copyOf(Collection c):使用一个普通集合来创建EnumSet集合。(注意:拷贝的集合中只能是枚举类型的元素,如果有其他类型的元素直接报错)
- EnumSet copyOf(EnumSet s):创建一个与指定EnumSet具有相同元素类型、相同集合元素的
EnumSet集合。 - EnumSet noneOf(Class elementType):创建一个元素类型为指定枚举类型的空EnumSet。
- EnumSet of(E first,E… rest):创建一个包含一个或多个枚举值的EnumSet集合,传入的多个枚举
值必须属于同一个枚举类。 - EnumSet range(E from,E to):创建一个包含从from枚举值到 to枚举值范围内所有枚举值的EnumSet集合。
enum Season
{
SPRING,SUMMER,FALL,WINTER
}
public class EnumSetTest
{
public static void main(String[] args)
{
// 创建一个EnumSet集合,集合元素就是Season枚举类的全部枚举值
EnumSet es1 = EnumSet.allOf(Season.class);
System.out.println(es1); // 输出[SPRING,SUMMER,FALL,WINTER]
// 创建一个EnumSet空集合,指定其集合元素是Season类的枚举值。
EnumSet es2 = EnumSet.noneOf(Season.class);
System.out.println(es2); // 输出[]
// 手动添加两个元素
es2.add(Season.WINTER);
es2.add(Season.SPRING);
System.out.println(es2); // 输出[SPRING,WINTER]
// 以指定枚举值创建EnumSet集合
EnumSet es3 = EnumSet.of(Season.SUMMER , Season.WINTER);
System.out.println(es3); // 输出[SUMMER,WINTER]
EnumSet es4 = EnumSet.range(Season.SUMMER , Season.WINTER);
System.out.println(es4); // 输出[SUMMER,FALL,WINTER]
// 新创建的EnumSet集合的元素和es4集合的元素有相同类型,
// es5的集合元素 + es4集合元素 = Season枚举类的全部枚举值
EnumSet es5 = EnumSet.complementOf(es4);
System.out.println(es5); // 输出[SPRING]
//拷贝集合
Collection c = new HashSet();
c.clear();
c.add(Season.FALL);
c.add(Season.SPRING);
// 复制Collection集合中所有元素来创建EnumSet集合
EnumSet enumSet = EnumSet.copyOf(c);
System.out.println(enumSet); // 输出[SPRING,FALL]
c.add("疯狂Java讲义");
c.add("轻量级Java EE企业应用实战");
// 下面代码出现异常:因为c集合里的元素不是全部都为枚举值
enumSet = EnumSet.copyOf(c);
}
}
Set实现类对比
-
HashSet的性能总是比TreeSet好(特别是最常用的添加、查询元素等操作),因为TreeSet需要额外的红黑树算法来维护集合元素的次序。只有当需要一个保持排序的Set时,才应该使用TreeSet,否则都应该使用HashSet。
-
HashSet的一个子类:LinkedHashSet,对于普通的插入、删除操作,LinkedHashSet 比 HashSet要略微慢一点,这是由维护链表所带来的额外开销造成的,但由于有了链表,遍历LinkedHashSet 会更快。
-
EnumSet 是所有Set实现类中性能最好的,但它只能保存同一个枚举类的枚举值作为集合元素。
-
Set的三个实现类HashSet、TreeSet和EnumSet都是线程不安全的。如果有多个线程同时访问一个Set集合,并且有超过一个线程修改了该Set集合,则必须手动保证该Set集合的同步性。可以通过Collections 工具类的 synchronizedSortedSet方法来“包装”该Set集合。此操作最好在创建时进行,以防止对Set集合的意外非同步访问。
-
SortedSet<Object> set = Collections.synchronizedSortedSet(new TreeSet<>()); NavigableSet<Object> objects = Collections.synchronizedNavigableSet(new TreeSet<>()); Collection<Object> sets = Collections.synchronizedCollection(new LinkedHashSet<>());
-
5. Map集合
Map用于保存具有映射关系的数据,因此Map集合里保存着两组值, key和value,key和 value都
可以是任何引用类型的数据。
特征:
-
Map的key不允许重复。
-
Map里的数据分为key数据组,value数据组,key和 value之间存在单向一对一关系,通过指定的key,总能找到唯一的value。从 Map中取出数据时,只要给出指定的key,就可以取出对应的value。
-
把Map里的所有key放在一起来看,它们就组成了一个Set集合(所有的key没有顺序,key与 key之间不能重复),Map包含了一个keySet()方法,用于返回Map里所有key组成的Set集合。
-
Map里 key集和Set集合里元素的存储形式很像,Map子类和Set子类在名字上也相似,Set接口下有HashSet、LinkedHashSet、SortedSet(接口)、TreeSet、EnumSet等子接口和实现类, Map接口下有 HashMap、LinkedHashMap、SortedMap(接口)、TreeMap、EnumMap等子接口和实现类。Map 的这些实现类和子接口中 key集的存储形式和对应Set集合中元素的存储形式完全相同。
-
Map的所有值类似于一个List,可以重复,可以根据索引去查。
Set与Map的关系:
Set与 Map 之间的关系非常密切。虽然Map中放的元素是key-value对,Set集合中放的元素是单个对象,但如果把key-value对中的value当成key的附庸: key在哪里, value就跟在哪里,这样就可以像对待Set一样来对待Map了。事实上,Map提供了一个Entry内部类来封装key-value对,而计算Entry存储时则只考虑Entry封装的key。Java是先实现了Map,然后通过包装一个所有 value都为null 的 Map就实现了Set集合。
常用方法:
-
void clear(:删除该Map对象中的所有key-value对。
-
boolean containsKey(Object key):查询Map中是否包含指定的 key,如果包含则返回 true。
-
boolean containsValue(Object value):查询Map中是否包含一个或多个value,如果包含则返回
true. -
Set entrySet():返回 Map中包含的 key-value对所组成的Set集合,每个集合元素都是Map.Entry(Entry是 Map 的内部类)对象。
-
Object put(Object key, Object value):添加一个key-value对,如果当前Map中已有一个与该key
相等的key-value对,则新的key-value对会覆盖原来的key-value对。 -
Object get(Object key):返回指定key所对应的value;如果此 Map中不包含该key,则返回null。
-
Object remove(Object key):删除指定key所对应的 key-value对,返回被删除key所关联的 value,如果该key不存在,则返回null。
-
Set keySet():返回该Map中所有key组成的Set集合。
-
boolean isEmpty():查询该Map是否为空(即不包含任何key-value对),如果为空则返回true。
-
void putAll(Map m):将指定Map中的key-value对复制到本Map 中。
-
int size():返回该Map里的key-value对的个数。
-
Collection values():返回该Map里所有 value组成的Collection。
Map接口提供的实现类
- HashMap
- Hashtable
- HashMap 的子类LinkedHashMap
- 还有SortedMap子接口及该接口的实现类TreeMap
- WeakHashMap 、ldentityHashMap等。
Map内部类Entry
该类封装了一个key-value对。Entry包含三个方法。
- Object getKey():返回该Entry里包含的key值。
- Object getValue(): 返回该Entry里包含的value值。
- Object setValue(V value):设置该Entry里包含的value值,并返回新设置的value值。
public class MapTest
{
public static void main(String[] args)
{
Map map = new HashMap();
// 成对放入多个key-value对
map.put("疯狂Java讲义" , 109);
map.put("疯狂iOS讲义" , 10);
map.put("疯狂Ajax讲义" , 79);
// 多次放入的key-value对中value可以重复
map.put("轻量级Java EE企业应用实战" , 99);
// 放入重复的key时,新的value会覆盖原有的value
// 如果新的value覆盖了原有的value,该方法返回被覆盖的value
System.out.println(map.put("疯狂iOS讲义" , 99)); // 输出10
System.out.println(map); // 输出的Map集合包含4个key-value对
// 判断是否包含指定key
System.out.println("是否包含值为 疯狂iOS讲义 key:"
+ map.containsKey("疯狂iOS讲义")); // 输出true
// 判断是否包含指定value
System.out.println("是否包含值为 99 value:"
+ map.containsValue(99)); // 输出true
// 获取Map集合的所有key组成的集合,通过遍历key来实现遍历所有key-value对
for (Object key : map.keySet() )
{
// map.get(key)方法获取指定key对应的value
System.out.println(key + "-->" + map.get(key));
}
map.remove("疯狂Ajax讲义"); // 根据key来删除key-value对。
System.out.println(map); // 输出结果不再包含 疯狂Ajax讲义=79 的key-value对
}
}
Java8 为Map新增的方法
- boolean remove(Object key, Object value):这是Java8新增的方法,删除指定key、value所对应的 key-value对。如果从该Map中成功地删除该key-value对,该方法返回 true,否则返回false。
- Object compute(Object key,BiFunction remappingFunction):该方法使用remappingFunction根据原key-value对计算一个新value。
- 只要新value不为null,就使用新value覆盖原value;
- 如果原value不为null,但新value为null,则删除原 key-value对;
- 如果原value、新value同时为null,那么该方法不改变任何key-value对,直接返回null。
- Object computelfAbsent(Object key,Function mappingFunction):如果传给该方法的 key参数在Map中对应的value为 null,则使用mappingFunction根据key计算一个新的结果,如果计算结果不为null,则用计算结果覆盖原有的value。如果原Map原来不包括该key,那么该方法可能会添加一组 key-value对。
- Object computeIfPresent(Object key, BiFunction remappingFunction):如果传给该方法的 key 参数在Map中对应的value不为null,该方法将使用remappingFunction根据原key、value计算一个新的结果,如果计算结果不为null,则使用该结果覆盖原来的 value;如果计算结果为null,则删除原key-value对。
- void forEach(BiConsumer action):该方法是Java 8为Map新增的一个遍历key-value对的方法,通过该方法可以更简洁地遍历Map 的key-value对。
- Object getOrDefault(Object key, V defaultValue):获取指定key对应的value。如果该key 不存在,则返回defaultValue。
- Object merge(Object key, Object value,BiFunction remappingFunction):该方法会先根据key 参数获取该Map中对应的value。如果获取的value为null,则直接用传入的value覆盖原有的value(在这种情况下,可能要添加一组key-value 对);如果获取的 value不为 null,则使用remappingFunction函数根据原value、新value计算一个新的结果,并用得到的结果去覆盖原有的value。
- Object putIfAbsent(Object key, Object value):该方法会自动检测指定key对应的value是否为null,如果该key对应的 value为null,该方法将会用新value 代替原来的null 值。
- Object replace(Object key, Object value):将Map中指定key对应的value替换成新value。与传统put)方法不同的是,该方法不可能添加新的key-value对。如果尝试替换的key在原Map中不存在,该方法不会添加 key-value对,而是返回null。
- boolean replace(K key, V oldValue,V newValue):将Map中指定key-value对的原value替换成新value。如果在 Map中找到指定的 key-value对,则执行替换并返回true,否则返回 false。
- replaceAll(BiFunction function):该方法使用BiFunction对原 key-value对执行计算,并将计算结果作为该key-value对的value值。
public class MapTest2
{
public static void main(String[] args)
{
Map map = new HashMap();
// 成对放入多个key-value对
map.put("疯狂Java讲义" , 109);
map.put("疯狂iOS讲义" , 99);
map.put("疯狂Ajax讲义" , 79);
// 尝试替换key为"疯狂XML讲义"的value,由于原Map中没有对应的key,
// 因此对Map没有改变,不会添加新的key-value对
map.replace("疯狂XML讲义" , 66);
System.out.println(map);
// 使用原value与参数计算出来的结果覆盖原有的value
map.merge("疯狂iOS讲义" , 10 ,
(oldVal , param) -> (Integer)oldVal + (Integer)param);
System.out.println(map); // "疯狂iOS讲义"的value增大了10
// 当key为"Java"对应的value为null(或不存在时),使用计算的结果作为新value
map.computeIfAbsent("Java" , (key)->((String)key).length());
System.out.println(map); // map中添加了 Java=4 这组key-value对
// 当key为"Java"对应的value存在时,使用计算的结果作为新value
map.computeIfPresent("Java",
(key , value) -> (Integer)value * (Integer)value);
System.out.println(map); // map中 Java=4 变成 Java=16
}
}
Java8改进的HashMap和Hashtable实现类
HashMap和 Hashtable都是 Map接口的实现类,类似于ArrayList和Vector的关系。
Java8改进了HashMap 的实现,使用HashMap存在 key冲突时依然具有较好的性能。
HashMap和Hashtable存储的数据都是乱序的。
Hashtable和 HashMap存在两点典型区别
- Hashtable是一个线程安全的 Map实现,但HashMap是线程不安全的实现,所以 HashMap 比Hashtable 的性能高一点;但如果有多个线程访问同一个Map对象时,虽然使用Hashtable实现类更好,但是现在通常使用:Collections.synchronizedMap(new HashMap<>());等方法
- Hashtable不允许使用null 作为 key和 value,如果试图把 null值放进Hashtable 中,将会引发NullPointerException异常;但HashMap可以使用null 作为key或 value。
- HashMap里的key不能重复,所以HashMap里最多只有一个key-value对的 key为null,但可以有无数多个key-value对的value为null。
存储null值
public class NullInHashMap
{
public static void main(String[] args)
{
HashMap hm = new HashMap();
// 试图将两个key为null的key-value对放入HashMap中
hm.put(null , null);
hm.put(null , null); // 报错 key重复
// 将一个value为null的key-value对放入HashMap中
hm.put("a" , null); //正常
// 输出Map对象
System.out.println(hm);
}
}
key和value相等的标准
-
在HashMap、Hashtable中存储、获取对象,用作key 的对象必须实现 hashCode()方法和equals()方法。
-
与HashSet一样,HashMap、Hashtable判断两个key 相等的标准也是:两个 key通过 equals()方法比较返回 true,两个key 的hashCode值也相等。
- 注意:
- 自定义类时,如果equals返回true,那么对应的hashcode也应该是true,二者保持一致。
- key一旦定义后,不能进行修改,否则可能造成和HashSet修改后一样的效果。
- 注意:
-
HashMap、Hashtable中包含一个containsValue()方法,用于判断是否包含指定的 value。判断两个value相等的标准是:两个对象通过equals()方法比较返回true即可。
class A
{
int count;
public A(int count)
{
this.count = count;
}
// 根据count的值来判断两个对象是否相等。
public boolean equals(Object obj)
{
if (obj == this)
return true;
if (obj != null && obj.getClass() == A.class)
{
A a = (A)obj;
return this.count == a.count;
}
return false;
}
// 根据count来计算hashCode值。
public int hashCode()
{
return this.count;
}
}
class B
{
// 重写equals()方法,B对象与任何对象通过equals()方法比较都返回true
public boolean equals(Object obj)
{
return true;
}
}
public class HashtableTest
{
public static void main(String[] args)
{
Hashtable ht = new Hashtable();
ht.put(new A(60000) , "疯狂Java讲义");
ht.put(new A(87563) , "轻量级Java EE企业应用实战");
ht.put(new A(1232) , new B());
System.out.println(ht);
// 只要两个对象通过equals比较返回true,
// Hashtable就认为它们是相等的value。
// 由于Hashtable中有一个B对象,
// 它与任何对象通过equals比较都相等,所以下面输出true。如果使用HashMap,则返回false,因为使用的是string的equals方法进行比较。
System.out.println(ht.containsValue("测试字符串")); // 输出true
// 只要两个A对象的count相等,它们通过equals比较返回true,且hashCode相等
// Hashtable即认为它们是相同的key,所以下面输出true。
System.out.println(ht.containsKey(new A(87563))); // 输出true
// 下面语句可以删除最后一个key-value对
ht.remove(new A(1232));
System.out.println(ht);
}
}
注意:
- 由于B的equals方法默认返回true,因此他和谁比都返回true。此时使用containsValue不管和谁比都返回true。
扩展:HashMap和Hashtable的containsValue机制的区别。
HashMap的处理:
Hashtable处理
key不能修改
- 在HashMap中使用可变对象作为Key带来的问题:如果HashMap Key的哈希值在存储键值对后发生改变,Map可能再也查找不到这个Entry了。
- 如果要使用可变对象作为key,那么我们要保证修改对象值的时候,保证对象的hashcode值不会随着对象的属性变化而变化
- 在HashMap中,使用String、Integer等不可变类型用作Key是非常明智的。
class A
{
int count;
public A(int count)
{
this.count = count;
}
// 根据count的值来判断两个对象是否相等。
public boolean equals(Object obj)
{
if (obj == this)
return true;
if (obj != null && obj.getClass() == A.class)
{
A a = (A)obj;
return this.count == a.count;
}
return false;
}
// 根据count来计算hashCode值。
public int hashCode()
{
return this.count;
}
}
public class HashMapErrorTest
{
public static void main(String[] args)
{
HashMap ht = new HashMap();
// 此处的A类与前一个程序的A类是同一个类
A a = new A(87563);
A b = new A(60000);
System.out.println("hashcode-a:"+a.hashCode()); //最初hashcode
System.out.println("hashcode-b:"+b.hashCode()); //最初hashcode
ht.put(a, "疯狂Java讲义");
ht.put(b , "轻量级Java EE企业应用实战");
// 获得Hashtable的key Set集合对应的Iterator迭代器
Iterator it = ht.keySet().iterator();
// 取出Map中第一个key,并修改它的count值
A first = (A)it.next();
first.count = 87563;
System.out.println("hashcode-b:"+first.hashCode()); //hashcode发生改变
System.out.println("hashcode-b:"+b.hashCode());
// 输出{A@1560b=疯狂Java讲义, A@1560b=轻量级Java EE企业应用实战}
System.out.println(ht);
// 只能删除没有被修改过的key所对应的key-value对
ht.remove(new A(87563));
System.out.println(ht);
// 无法获取剩下的value,下面两行代码都将输出null。
System.out.println(ht.get(new A(87563)));
System.out.println(ht.get(new A(60000)));
}
}
解析:debug可知
- first.count修改的是60000的key。
- 删除87563时,只有a 的hashcode和equals的值都返回true,因此删除的是最初添加的a
- 获取new A(87563)时,虽然此时hashmap中还有一个key,他的hashcode和equals与new A(87563)都相等,但是我们使用迭代器获取的时候却直接报错,debug打印,首节点为null,可能是这个原因造成判断通过不了
- 最后获取值时,因为new A(60000)的key已经被修改,所以他的hashcode值发生变化,但是他的equals也发生变化,因此查询不到。
LinkedHashMap实现类
HashMap有一个LinkedHashMap子类:LinkedHashMap也使用双向链表来维护 key-value对的次序(其实只需要考虑key 的次序),该链表负责维护Map的迭代顺序,迭代顺序与key-value对的插入顺序保持一致。
LinkedHashMap可以避免对HashMap、Hashtable里的 key-value对进行排序(只要插入key-value对时保持顺序即可),同时又可避免使用TreeMap所增加的成本。
LinkedHashMap需要维护元素的插入顺序,因此性能略低于HashMap 的性能;但因为它以链表来维护内部顺序,所以在迭代访问Map里的全部元素时将有较好的性能。
public class LinkedHashMapTest
{
public static void main(String[] args)
{
LinkedHashMap scores = new LinkedHashMap();
scores.put("语文" , 80);
scores.put("英文" , 82);
scores.put("数学" , 76);
// 调用forEach方法遍历scores里的所有key-value对
scores.forEach((key, value) -> System.out.println(key + "-->" + value));
}
}
语文-->80
英文-->82
数学-->76
Properties 读写属性文件
Properties类是 Hashtable类的子类,该对象在处理属性文件时特别方便( Windows操作平台上的 ini 文件就是一种属性文件,或者xml)。Properties类可以把 Map对象和属性文件关联起来,从而可以把 Map对象中的 key-value对写入属性文件中,也可以把属性文件中的“属性名=属性值”加载到Map对象中。由于属性文件里的属性名、属性值只能是字符串类型,所以 Properties里的 key.value都是字符串类型。Properties相当于一个key、 value都是String类型的Map.
- String getProperty(String key):获取 Properties中指定属性名对应的属性值,类似于Map 的get(Object key)方法。
- String getProperty(String key, String defaultValue):该方法与前一个方法基本相似。该方法多一个功能,如果 Properties中不存在指定的key 时,则该方法指定默认值。
- Object setProperty(String key, String value):设置属性值,类似于Hashtable的 put()方法。除此之外,它还提供了两个读写属性文件的方法。
- void load(InputStream inStream):从属性文件(以输入流表示)中加载 key-value对,把加载到的key-value对追加到 Properties里(Properties是Hashtable的子类,它不保证key-value对之间的次序)。
- void store(OutputStream out, String comments):将Properties中的 key-value对输出到指定的属性文件(以输出流表示)中。
public class PropertiesTest
{
public static void main(String[] args)
throws Exception
{
Properties props = new Properties();
// 向Properties中增加属性
props.setProperty("username" , "yeeku");
props.setProperty("password" , "123456");
// 将Properties中的key-value对保存到a.ini文件中
props.store(new FileOutputStream("a.ini")
, "comment line");
// 新建一个Properties对象
Properties props2 = new Properties();
// 向Properties中增加属性
props2.setProperty("gender" , "male");
// 将a.ini文件中的key-value对追加到props2中
props2.load(new FileInputStream("a.ini") );
System.out.println(props2);
}
}
{password=123456, gender=male, username=yeeku}
SortedMap和TreeMap实现类
Map接口派生出一个 SortedMap子接口,SortedMap接口有一个TreeMap 实现类。
TreeMap就是一个红黑树数据结构,每个key-value对即作为红黑树的一个节点。TreeMap存储key-value对(节点)时,需要根据key对节点进行排序。TreeMap可以保证所有的 key-value对处于有序状态。TreeMap也有两种排序方式。
- 自然排序:TreeMap的所有key必须实现Comparable接口,而且所有的 key应该是同一个类的对象,否则将会抛出ClassCastException异常。
- 定制排序:创建TreeMap时,传入一个Comparator对象,该对象负责对TreeMap中的所有 key进行排序。采用定制排序时不要求Map 的key实现Comparable接口。
类似于TreeSet 中判断两个元素相等的标准,TreeMap中判断两个 key 相等的标准是:两个key 通过compareTo()方法返回0,TreeMap即认为这两个key是相等的。
使用自定义类作为TreeMap 的 key,且想让TreeMap良好地工作,则重写该类的equals()方法和 compareTo()方法时应保持一致的返回结果:**两个 key通过equals()方法比较返回 true时,它们通过compareTo()方法比较应该返回0。**如果equals()方法与compareTo()方法的返回结果不一致,TreeMap与Map接口的规则就会冲突。
获取前一个或后一个
- Map.Entry firstEntry():返回该Map中最小key所对应的 key-value对,如果该Map 为空,则返回null。
- Object firstKey():返回该Map中的最小key’值,如果该Map为空,则返回null。
- Map.Entry lastEntry():返回该Map中最大key 所对应的 key-value对,如果该Map为空或不存在这样的key-value对,则都返回null。
- Object lastKey():返回该Map中的最大key值,如果该Map为空或不存在这样的 key,则都返回null。
- Map.Entry higherEntry(Object key):返回该Map中位于key后一位的 key-value对(即大于指定key的最小key所对应的key-value对)。如果该Map为空,则返回null。
- Object higherKey(Object key):返回该Map中位于key后一位的 key值(即大于指定key 的最小key值)。如果该Map为空或不存在这样的key-value对,则都返回null。
- Map.Entry lowerEntry(Object key):返回该Map中位于key前一位的 key-value对(即小于指定key的最大key所对应的 key-value对)。如果该Map为空或不存在这样的 key-value对,则都返回null。
- Object lowerKey(Object key):返回该Map中位于key前一位的key值(即小于指定key的最大key值)。如果该Map为空或不存在这样的key,则都返回null。
截取相关子Map
- NavigableMap subMap(Object fromKey, boolean fromInclusive, Object toKey, boolean toInclusive):返回该Map的子Map,其 key 的范围是从fromKey(是否包括取决于第二个参数)到toKey(是否包括取决于第四个参数)。
- SortedMap subMap(Object fromKey,Object toKey):返回该Map的子Map,其 key的范围是从fromKey(包括)到toKey(不包括)。
- SortedMap tailMap(Object fromKey):返回该Map的子Map,其 key的范围是大于fromKey(包括)的所有key.
- NavigableMap tailMap(Object fromKey, boolean inclusive):返回该Map的子Map,其 key的范围是大于fromKey(是否包括取决于第二个参数)的所有key.
- SortedMap headMap(Object toKey):返回该Map的子Map,其 key 的范围是小于toKey(不包括)的所有key.
- NavigableMap headMap(Object toKey, boolean inclusive):返回该Map的子Map,其 key的范围是小于toKey(是否包括取决于第二个参数)的所有key。
class R implements Comparable
{
int count;
public R(int count)
{
this.count = count;
}
public String toString()
{
return "R[count:" + count + "]";
}
// 根据count来判断两个对象是否相等。
public boolean equals(Object obj)
{
if (this == obj)
return true;
if (obj != null && obj.getClass() == R.class)
{
R r = (R)obj;
return r.count == this.count;
}
return false;
}
// 根据count属性值来判断两个对象的大小。
public int compareTo(Object obj)
{
R r = (R)obj;
return count > r.count ? 1 :
count < r.count ? -1 : 0;
}
}
public class TreeMapTest
{
public static void main(String[] args)
{
TreeMap tm = new TreeMap();
tm.put(new R(3) , "轻量级Java EE企业应用实战");
tm.put(new R(-5) , "疯狂Java讲义");
tm.put(new R(9) , "疯狂Android讲义");
System.out.println(tm);
// 返回该TreeMap的第一个Entry对象
System.out.println(tm.firstEntry());
// 返回该TreeMap的最后一个key值
System.out.println(tm.lastKey());
// 返回该TreeMap的比new R(2)大的最小key值。
System.out.println(tm.higherKey(new R(2)));
// 返回该TreeMap的比new R(2)小的最大的key-value对。
System.out.println(tm.lowerEntry(new R(2)));
// 返回该TreeMap的子TreeMap
System.out.println(tm.subMap(new R(-1) , new R(4)));
}
}
WeakHashMap实现类
WeakHashMap 与 HashMap的用法基本相似。
WeakHashMap 与HashMap 的区别:
HashMap 的 key 保留了对实际对象的强引用,这意味着只要该HashMap对象不被销毁,该HashMap的所有key所引用的对象就不会被垃圾回收,HashMap也不会自动删除这些key所对应的 key-value对;但WeakHashMap 的 key只保留了对实际对象的弱引用,这意味着如果WeakHashMap对象的key所引用的对象没有被其他强引用变量所引用,则这些key 所引用的对象可能被垃圾回收,WeakHashMap也可能自动删除这些key 所对应的key-value对。
**弱引用:**可以理解成匿名对象
WeakHashMap中的每个key对象只持有对实际对象的弱引用,因此,当垃圾回收了该key所对应的实际对象之后,WeakHashMap会自动删除该key对应的key-value对。
public class WeakHashMapTest
{
public static void main(String[] args)
{
WeakHashMap whm = new WeakHashMap();
// 将WeakHashMap中添加三个key-value对,
// 三个key都是匿名字符串对象(没有其他引用)
whm.put(new Employee(1L,"123", LocalDate.now()) , new String("良好"));
whm.put(new Employee(2L,"123", LocalDate.now()), new String("及格"));
whm.put(new Employee(3L,"123", LocalDate.now()), new String("中等"));
//将 WeakHashMap中添加一个key-value对,
// 该key是一个系统缓存的字符串对象。
Employee employee = new Employee(3L, "123", LocalDate.now());
whm.put(employee , new String("中等"));
// 输出whm对象,将看到4个key-value对。
System.out.println(whm);
// 通知系统立即进行垃圾回收
System.gc();
System.runFinalization();
// 通常情况下,将只看到一个key-value对。
System.out.println(whm);
}
}
1. {Employee [id=3, name=123, dob=2021-11-28]=中等, Employee [id=3, name=123, dob=2021-11-28]=中等, Employee [id=1, name=123, dob=2021-11-28]=良好, Employee [id=2, name=123, dob=2021-11-28]=及格}
2. {Employee [id=3, name=123, dob=2021-11-28]=中等}
解析:
因为以下我们添加的三个Employee对象都是匿名对象,所以并没有任何强引用指向他们,因此垃圾回收的时候,会把这些弱引用回收掉。
whm.put(new Employee(1L,"123", LocalDate.now()) , new String("良好"));
whm.put(new Employee(2L,"123", LocalDate.now()), new String("及格"));
whm.put(new Employee(3L,"123", LocalDate.now()), new String("中等"));
IdentityHashMap实现类
IdentityHashMap的实现机制与 HashMap基本相似
区别:
IdentityHashMap在处理两个 key 相等时,当且仅当两个key严格相等(keyl ==key2)时,IdentityHashMap才认为两个key相等;普通的 HashMap而言,只要keyl和 key2通过equals()方法比较返回true,且它们的 hashCode值相等即可。
public class IdentityHashMapTest
{
public static void main(String[] args)
{
IdentityHashMap ihm = new IdentityHashMap();
// 下面两行代码将会向IdentityHashMap对象中添加两个key-value对
ihm.put(new String("语文") , 89);
ihm.put(new String("语文") , 78);
// 下面两行代码只会向IdentityHashMap对象中添加一个key-value对
ihm.put("java" , 93);
ihm.put("java" , 98);
System.out.println(ihm);
}
}
因为我们new了两个String,他们的地址值不一样,所以可以添加成功。但是添加两个java时,二者都是字符串常量池中的变量,因此二者相等,所以后来添加的会覆盖掉之前的。
EnumMap实现类
EnumMap是一个与枚举类一起使用的Map实现,EnumMap中的所有key都必须是单个枚举类的枚举值。创建EnumMap时必须显式或隐式指定它对应的枚举类。
特点
- EnumMap在内部以数组形式保存,所以这种实现形式非常紧凑、高效。
- EnumMap根据key的自然顺序(即枚举值在枚举类中的定义顺序)来维护key-value对的顺序。当程序通过keySet()、entrySet()、values()等方法遍历EnumMap 时可以看到这种顺序。
- EnumMap**不允许使用null 作为key,但允许使用null 作为 value。**如果试图使用null 作为key时将抛出 NullPointerException异常。如果只是查询是否包含值为null 的 key,或只是删除值为null 的key,都不会抛出异常。
enum Season
{
SPRING,SUMMER,FALL,WINTER
}
public class EnumMapTest
{
public static void main(String[] args)
{
// 创建EnumMap对象,该EnumMap的所有key都是Season枚举类的枚举值
EnumMap enumMap = new EnumMap(Season.class);
enumMap.put(Season.SUMMER , "夏日炎炎");
enumMap.put(Season.SPRING , "春暖花开");
System.out.println(enumMap);
}
}
{SPRING=春暖花开, SUMMER=夏日炎炎} 按照春夏排序。
Map实现类对比
-
HashMap和 Hashtable的实现机制几乎一样,但由于 Hashtable是一个古老的、线程安全的集合,因此HashMap通常比 Hashtable要快。
-
TreeMap(大小顺序)通常比HashMap、Hashtable要慢(尤其在插入、删除 key-value对时更慢),因为TreeMap底层采用红黑树来管理key-value对(红黑树的每个节点就是一个key-value对)。
使用TreeMap有一个好处:TreeMap中的 key-value对总是处于有序状态,无须专门进行排序操作。当TreeMap被填充之后,就可以调用keySet(),取得由 key组成的Set,然后使用toArray()方法生成key的数组,接下来使用Arrays 的 binarySearch()方法在已排序的数组中快速地查询对象。 -
一般的应用场景,应该多考虑使用HashMap,因为 HashMap正是为快速查询设计的(HashMap底层其实也是采用数组来存储key-value对)。但如果程序需要一个总是排好序的Map时,则可以考虑使用TreeMap。
-
LinkedHashMap (添加顺序)比 HashMap慢一点,因为它需要维护链表来保持Map中 key-value时的添加顺序。
-
ldentityHashMap性能没有特别出色之处,采用与HashMap基本相似的实现,使用==而不是equals()方法来判断元素相等。
-
EnumMap 的性能最好,只能使用同一个枚举类的枚举值作为 key。
HashSet与HashMap的性能分析(了解)
对于HashSet 及其子类而言,它们采用hash算法来决定集合中元素的存储位置,并通过 hash算法来控制集合的大小;对于HashMap、Hashtable及其子类而言,它们采用hash算法来决定Map 中 key的存储,并通过hash算法来增加 key集合的大小。
hash表里可以存储元素的位置被称为“桶( bucket)”,在通常情况下,单个“桶”里存储一个元素,此时有最好的性能: hash算法可以根据hashCode值计算出“桶”的存储位置,接着从“桶”中取出元素。但hash表的状态是open的:在发生“hash 冲突”的情况下,单个桶会存储多个元素,这些元素以链表形式存储,必须按顺序搜索。
“hash 冲突”的示意图。
因为HashSet和HashMap、Hashtable都使用hash算法来决定其元素(HashMap则只考虑key)的存储,因此HashSet、HashMap的hash表包含如下属性。
-
容量(capacity ): hash 表中桶的数量。
-
初始化容量(initial capacity):创建hash表时桶的数量。HashMap和HashSet 都允许在构造器中指定初始化容量。
-
尺寸( size):当前hash表中记录的数量。
-
负载因子(load factor):负载因子等于“size/capacity"”。负载因子为0,表示空的 hash表,0.5
表示半满的 hash表,依此类推。轻负载的hash表具有冲突少、适宜插入与查询的特点(但是使用Iterator迭代元素时比较慢)。
HashSet和 HashMap、Hashtable的构造器允许指定一个负载极限,HashSet和 HashMap、Hashtable默认的“负载极限”为0.75,这表明当该hash表的3/4已经被填满时,hash表会发生rehashing
“负载极限”的默认值(0.75)是时间和空间成本上的一种折中:较高的“负载极限”可以降低 hash表所占用的内存空间,但会增加查询数据的时间开销,而查询是最频繁的操作(HashMap 的get()与 put()方法都要用到查询);较低的“负载极限”会提高查询数据的性能,但会增加 hash表所占用的内存开销。程序员可以根据实际情况来调整HashSet和 HashMap的“负载极限”值。
如果开始就知道HashSet和 HashMap、Hashtable 会保存很多记录,则可以在创建时就使用较大的初始化容量,如果初始化容量始终大于HashSet和 HashMap、Hashtable所包含的最大记录数除以“负载极限”,就不会发生rehashing。使用足够大的初始化容量创建HashSet和HashMap、Hashtable时,可以更高效地增加记录,但将初始化容量设置太高可能会浪费空间,因此通常不要将初始化容量设置得过高。
6. 工具类:Collections
排序操作
- void reverse(List list):反转指定List集合中元素的顺序。
- void shuffle(List list):对List集合元素进行随机排序(shuffle方法模拟了“洗牌”动作)。
- void sort(List list):根据元素的自然顺序对指定List集合的元素按升序进行排序。
- void sort(List list, Comparator c):根据指定Comparator产生的顺序对List集合元素进行排序。
- void swap(List list, int i, int j):将指定List集合中的i处元素和j处元素进行交换。
- void rotate(List list , int distance):当distance为正数时,将list集合的后distance个元素“整体”移到前面;当distance为负数时,将list集合的前distance个元素“整体”移到后面。该方法不会改变集合的长度。
public class SortTest
{
public static void main(String[] args)
{
ArrayList nums = new ArrayList();
nums.add(2);
nums.add(-5);
nums.add(3);
nums.add(0);
System.out.println(nums); // 输出:[2, -5, 3, 0]
Collections.reverse(nums); // 将List集合元素的次序反转
System.out.println(nums); // 输出:[0, 3, -5, 2]
Collections.sort(nums); // 将List集合元素的按自然顺序排序
System.out.println(nums); // 输出:[-5, 0, 2, 3]
Collections.shuffle(nums); // 将List集合元素的按随机顺序排序
System.out.println(nums); // 每次输出的次序不固定
}
}
查找,替换操作
查询
- int binarySearch(List list, Object key):使用二分搜索法搜索指定的List集合,以获得指定对象在List集合中的索引。如果要使该方法可以正常工作,则必须保证List 中的元素已经处于有序状态。
获取最大最小元素
- Object max(Collection coll):根据元素的自然顺序,返回给定集合中的最大元素。
- Object max(Collection coll, Comparator comp):根据Comparator指定的顺序,返回给定集合中的最大元素。
- Object min(Collection coll):根据元素的自然顺序,返回给定集合中的最小元素。
- Object min(Collection coll, Comparator comp):根据Comparator指定的顺序,返回给定集合中的最小元素。
其他
- void fill(List list, Object obj):使用指定元素obj替换指定List集合中的所有元素。
- int frequency(Collection c, Object o):返回指定集合中指定元素的出现次数。
- int indexOfSubList(List source, List target):返回子List对象在父List对象中第一次出现的位置索引;如果父List中没有出现这样的子List,则返回-1。
- int lastIndexOfSubList(List source,List target):返回子List对象在父List对象中最后一次出现的位置索引;如果父List中没有出现这样的子List,则返回-1。
- boolean replaceAll(List list, Object oldVal, Object newVal):使用一个新值newVal替换List对象的所有旧值 oldVal。
public class SearchTest
{
public static void main(String[] args)
{
ArrayList nums = new ArrayList();
nums.add(2);
nums.add(-5);
nums.add(3);
nums.add(0);
System.out.println(nums); // 输出:[2, -5, 3, 0]
System.out.println(Collections.max(nums)); // 输出最大元素,将输出3
System.out.println(Collections.min(nums)); // 输出最小元素,将输出-5
Collections.replaceAll(nums , 0 , 1); // 将nums中的0使用1来代替
System.out.println(nums); // 输出:[2, -5, 3, 1]
// 判断-5在List集合中出现的次数,返回1
System.out.println(Collections.frequency(nums , -5));
Collections.sort(nums); // 对nums集合排序
System.out.println(nums); // 输出:[-5, 1, 2, 3]
//只有排序后的List集合才可用二分法查询,输出3
System.out.println(Collections.binarySearch(nums , 3));
}
}
线程同步
Collections类中提供了多个synchronizedXxx()方法,该方法可以将指定集合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题。
Java中常用的集合框架中的实现类HashSet、TreeSet、ArrayList、ArrayDeque、LinkedList、HashMap和TreeMap都是线程不安全的。如果有多个线程访问它们,而且有超过一个的线程试图修改它们,则存在线程安全的问题。Collections提供了多个类方法可以把它们包装成线程同步的集合。
public class SynchronizedTest
{
public static void main(String[] args)
{
// 下面程序创建了四个线程安全的集合对象
Collection c = Collections
.synchronizedCollection(new ArrayList());
List list = Collections.synchronizedList(new ArrayList());
Set s = Collections.synchronizedSet(new HashSet());
Map m = Collections.synchronizedMap(new HashMap());
}
}
设置不可变集合
不可变集合只能访问集合元素,不可修改集合元素。
- emptyXxx():返回一个空的、不可变的集合对象,此处的集合既可以是List,也可以是SortedSet,Set,还可以是Map、SortedMap等。
- singletonXxx():返回一个只包含指定对象(只有一个或一项元素)的、不可变的集合对象,此处的集合既可以是List,还可以是Map-
- unmodifiableXxx():返回指定集合对象的不可变视图,此处的集合既可以是List,也可以是Set、SortedSet,还可以是 Map、SorteMap等。
public class UnmodifiableTest
{
public static void main(String[] args)
{
// 创建一个空的、不可改变的List对象
List unmodifiableList = Collections.emptyList();
// 创建一个只有一个元素,且不可改变的Set对象
Set unmodifiableSet = Collections.singleton("疯狂Java讲义");
// 创建一个普通Map对象
Map scores = new HashMap();
scores.put("语文" , 80);
scores.put("Java" , 82);
// 返回普通Map对象对应的不可变版本
Map unmodifiableMap = Collections.unmodifiableMap(scores);
// 下面任意一行代码都将引发UnsupportedOperationException异常
unmodifiableList.add("测试元素");
unmodifiableSet.add("测试元素");
unmodifiableMap.put("语文" , 90);
}
}
扩展:
Comparable接口
Java提供了一个Comparable接口,该接口里定义了一个compareTo(Object obj)方法,该方法返回一个整数值,实现该接口的类必须实现该方法,实现了该接口的类的对象就可以比较大小。当一个对象调用该方法与另一个对象进行比较时,例如 obj1.compareTo(obj2),如果该方法返回0,则表明这两个对象相等;如果该方法返回一个正整数,则表明objl大于obj2;如果该方法返回一个负整数,则表明obj1小于obj2。
Java 的一些常用类已经实现了Comparable接口,并提供了比较大小的标准。下面是实现了Comparable接口的常用类。
BigDecimal、BigInteger 以及所有的数值型对应的包装类:按它们对应的数值大小进行比较。
Character:按字符的UNICODE值进行比较。
Boolean: true对应的包装类实例大于false对应的包装类实例。
String:按字符串中字符的UNICODE值进行比较。
Date、Time:后面的时间、日期比前面的时间、日期大。
Comparable和Comparator区别
Java基础系列-Comparable和Comparator - 简书 (jianshu.com)
(87条消息) java comparator 升序、降序、倒序从源码角度理解_山鬼谣的专栏-CSDN博客_comparator 倒序
Comparator的默认升序:
< return -1
= return 0
“>” return 1
Comparator的默认降序:
< return 1
= return 0
“>” return -1