程序中常有收集对象的需求,就目前为止,你学过可以收集对象的方式就是使用Object数组。

在Java SE中,其实就提供了数个收集对象的类,你可以直接取用这些类。

1 使用Collection收集对象

1. 1 认识Collection架构

Java SE提供了满足各种需求的API,在使用这些API前,建议先了解其继承与接口操作架构,才能了解何时该采用哪个类,以及类之间如何彼此合作。

Collection API接口继承架构设计如下图

image-20190621211655081

一般的收集对象的行为都是定义在java.util.Collection中。

  • 新增对象的add()方法
  • 移除对象的remove()方法
  • 是否包含对象的contains()方法

当我们需要逐一取得对象的时候,就需要迭代器, 定义在java.util.Iterator。

收集对象的共同行为定义在Collection中,然而收集对象会有不同的需求。

  • 如果希望收集时记录每个对象的索引顺序,并可依索引取回对象,这样的行为定义在java.util.List接口中。
  • 如果希望收集的对象不重复,具有集合的行为,则由java.util.Set定义。
  • 如果希望收集对象时可以队列方式,收集的对象加入至尾端,取得对象时可以从前端,则可以使用java.util.Queue。
  • 如果希望可以对Queue的两端进行加入、移除等操作,则可以使用java.util.Deque。

收集对象时,会依需求使用不同的接口操作对象。

比如,如果想要收集时具有索引顺序,

  • 一种办法是使用数组,
  • 使用Collection API里的List- java.util.ArrayList。如果查看API文件,会发现有以下继承与架构,

image-20190621212726532

Java SE API不仅提供许多已操作类,也考虑了我们自行扩充API的需求,以收集对象的基本行为来说,

其提供了

  • java.util.AbstractCollection操作了Collection基本行为,
  • java.util.AbstractList操作了List基本行为,必要时,可以继承AbstractCollection操作自己的Collection,继承AbstractList操作自己的List,这会比直接操作Collection或List接口方便许多。

image-20190621214401405

下面我们就详细看一下具有索引的List。

1.2 具有索引的List

List是一种Collection,作用是收集对象,并以索引方式保留收集的对象顺序,

最常使用的List实现类就是java.util.ArrayList。

List Collections

下图是一个简单的实例。

package com.ripjava.collection;

import java.util.*;

public class Guest {
    public static void main(String[] args) {
        List list = new java.util.ArrayList();
        Scanner scanner = new Scanner(System.in);
        String name;
        while(true) {
            System.out.print("访客姓名:");
            name = scanner.nextLine();
            if(name.equals("quit")) {
                break;
            }
          	// 添加
            list.add(name);
        }
        System.out.println("所有的访客:");
        foreach(list);
    }

    private static void foreach(List list) {
        for(int i = 0; i < list.size(); i++) {
            //获取
            String guest = (String) list.get(i);
            System.out.println(guest.toUpperCase());
        }
    }
}

查看API文档,你可能会发现List接口定义了add()、remove()、set()等许多和索引相关操作的方法。

在上面的图中我们java.util.LinkedList也实现了List接口。

下面将上面的

 package com.ripjava.collection;
 
 import java.util.*;
 
 public class Guest {
     public static void main(String[] args) {
         List list = new java.util.ArrayList();
         Scanner scanner = new Scanner(System.in);
         String name;
         while(true) {
             System.out.print("访客姓名:");
             name = scanner.nextLine();
             if(name.equals("quit")) {
                 break;
             }
             // 添加
             list.add(name);
         }
         System.out.println("所有的访客:");
         foreach(list);
     }
 
     private static void foreach(List list) {
         for(int i = 0; i < list.size(); i++) {
             //获取
             String guest = (String) list.get(i);
             System.out.println(guest.toUpperCase());
         }
     }
 }
 

查看API文档,你可能会发现List接口定义了add()、remove()、set()等许多和索引相关操作的方法。

在上面的图中我们java.util.LinkedList也实现了List接口。

练习(10分钟)

下面将上面Guest类中的替换为ArrayList换为LinkedList。结果还会一致吗?

练习之后我们结果是不变的、那么什么时候该用ArrayList?何时该用LinkedList呢?

1.2 .1 ArrayList特性

ArrayList内部就是使用Object数组来保存收集的对象。

数组在内存中会是连续的线性空间、

  • 如果要求索引随机存取速度时,像是排序,就可使用ArrayList,可得到较好的速度表现。

  • 如果需要调整索引顺序时,会有较差的表现。

    例如若在已收集100对象的ArrayList中,使用可指定索引的add()方法,将对象新增到索引0位置,那么原先索引0的对象必须调整至索引1、索引1的对象必须调整至索引2、索引2的对象必须调整至索引3。

    依此类推,使用ArrayList做这类操作成本会很高。

  • 数组的长度固定,在ArrayList内部数组长度不够时,会建立新数组,并将旧数组的参考指定给新数组,这也是必需耗费时间与内存的操作,为此,ArrayList有个可指定容量(Capacity)的构造函数,如果大概知道将收集的对象多少,可以在创建ArrayList时指定足够长度的内部数组。

    public ArrayList(int initialCapacity) {}
    
练习(20分钟)
  1. 请自己使用Object数组实现一个SimpleArrayList。包含以下的功能。
  • 无参数构造函数

    SimpleArrayList

    使用默认的容量(Capacity)初始化内部数组。

  • 指定初始化数组大小的构造函数

    SimpleArrayList(int capacity)

    使用指定的容量(Capacity)初始化内部数组。

  • add方法

    public void add(Object o)

    向内部数据添加Object,如果达到数组长度,请使用Arrays.copyOf将数组复制到一个新的数据。新的数组的长度是旧数组的 2倍。

  • get方法

    public Object get(int index)

    获取当前索引的对象。

  • size方法

    public int size()

    返回当前List的长度。

  1. 通过自己实现的SimpleArrayList,回头看一下 ArrayList特性。
1.2.2 LinkedList特性

LinkedList在操作List接口时,采用了双向链表结构。

链表是一种递归的数据结构,它或者为空(null),或者是指向一个结点(node)的引用,该节点还有一个元素和一个指向另一条链表的引用。

先给大家讲一下,单向链表的例子。

image-20190621221954427

那如果我们向链表中添加数据呢?

image-20190621224339469

下面我们来总结一下

  • 如果收集的对象经常会有变动索引的情况, 使用LinkedList比较好。
  • 如果要求索引随机存取速度的话,链表方式需要从第一个元素开始查找下一个元素的方式来查找指定索引的元素,会比较没有效率,像排序就不适合使用LinkedList。
练习(20分钟)

请自己实现一个SimpleLinkedList。包含以下的功能。

  • 私有内部类 Node

    • 公开成员变量

      Object o 指定当前节点的存储的元素

      Node next 下一个节点的引用(参考)

    • 构造函数

      Node(Object o) 可以指定要存储的元素

  • 成员变量

    private Node first 第一个节点

  • add方法

    public void add(Object o) 添加一个元素。

    • 如果first为null, 创建一个Node对象,并赋值给first变量。
    • 如果first不为null:
      • 声明一个变量last来保存当前List的最后一个节点,默认为 第一个节点。
      • 使用while循环找到最后一个节点(最后一个节点的next应该为null)。
        • 如果当前节点的next为null,那么就是最后一个节点
        • 如果当前节点的next不为null。 将当前节点的next节点赋值给last。直到找到真正最后一个节点。
      • 创建一个Node对象,并赋值last对象的next变量
  • size方法

    public int size() 返回当前List的长度。

    • 声明一个int变量count,保存List的长度,默认为0.
    • 声明一个Node类变量last来保存当前List的最后一个元素,默认为 第一个元素。
    • 使用while循环找到最后一个节点(最后一个节点的next应该为null)。找到之后赋值给last
      • 如果当前节点的next为null,那么就是最后一个节点
      • 如果当前节点的next不为null。 将当前节点的next节点赋值给last。并将count加1, 直到找到真正最后一个节点。
    • 返回int变量count。
  • get方法

    public Object get(int index) 获取当前索引的对象。

    • 声明一个变量size,将调用size()方法的返回值赋值给count。
    • 如果参数index的值大于或等于size, 请抛出IndexOutOfBoundsException
    • 使用while来找到对应index的节点,并将找到节点的存储的对象返回。

参考答案(有bug哦)

public class SimpleLinkedList {
	private class Node {
		Node(Object o) {
			this.o = o;
		}
		Object o;
		Node next;
	}

	private Node first;

	public void add(Object o) {
		if (first == null) {
			first = new Node(o);
		} else {
			Node last = first;
			while (last.next != null) {
				last = last.next;
			}
			last.next = new Node(o);
		}
	}

	public int size() {
		int count = 0;
		Node last = first;
		while (last.next != null) {
			last = last.next;
			count++;
		}
		return count;
	}

	public Object get(int index) {
		int size = size();
		if (index >= size) {
			throw new IndexOutOfBoundsException(String.format("Index: %d, Size: %d", index, size));
		}
		int count = 0;
		Node last = first;
		while (count < index) {
			last = last.next;
			count++;
		}
		return last.o;
	}
}

1.3 内容不重复的Set

同样是收集对象,在收集过程中若有相同对象,则不再重复收集,若有这类需求,可以使用Set接口的操作对象。

下面我们来看一个简单的例子。

1.3.1 HashSet

若有一个字符串,当中有许多的英文单词,你希望知道不重复的单词有几个,我们写了如下程序:

package com.ripjava.collection;

import java.util.HashSet;
import java.util.Scanner;
import java.util.Set;

public class Words {
	public static void main(String[] args) {
        Set words = new HashSet();
        Scanner scanner = new Scanner(System.in);
        System.out.print("请输入英文单词, 空格分割:");
        String line = scanner.nextLine();
        String[] tokens = line.split(" ");
        for(String token : tokens) {
            words.add(token);
        }
        System.out.printf("不重复的单词有 %d 和:%s%n",
                words.size(), words);
    }
}

  • String的split()方法,可以指定切割字符串的方式,在这里指定以空格切割,split()会返回String[],包括切割的每个字符串。
  • 接着将String[]中的每个字符串加入Set的操作HashSet中➋。由于Set的特性是不重复,因此若有相同单词,则不会再重复加入。
  • 最后只要调用Set的size()方法,就可以知道收集的字符串个数。
  • 最后的最后HashSet的toString()操作出力Set的元素。
请输入英文单词, 空格分割:a a b a a a a a a
不重复的单词有 2 和:[a, b]

下面我们开始翻车表演。

我们先在自己定义一个 学生类。

import java.util.HashSet;
import java.util.Set;

class Student {
    private String name;
    private String number;
    Student(String name, String number) {
        this.name = name;
        this.number = number;
    }
    
    @Override
    public String toString()  {
        return String.format("(%s, %s)", name, number);
    }
}

public class Students {
    public static void main(String[] args) {
        Set set = new HashSet();
        set.add(new Student("Justin", "B835031"));
        set.add(new Student("Monica", "B835032"));
        set.add(new Student("Justin", "B835031"));
        System.out.println(set);
    }
}

程序中使用Set收集了Student对象,其中故意重复加入了相同的学生数据,然而在执行结果中看到,Set并没有将重复的学生数据排除:

[(Justin, B835031), (Monica, B835032), (Justin, B835031)]

为什么呢?

因为没有告诉Set,什么样的Student实例才算是重复。

那我们先来看一下HashSet是怎么判断重复的。

使用对象的hashCode()与equals()来判断对象是否相同。

HashSet的操作概念是,在内存中创建空间,每个空间会有个哈希编码(Hash code)

image-20190621233311211

这些空间称为哈希桶(Hash bucket),如果对象要加入HashSet,则会调用对象的hashCode()取得哈希码,并尝试放入对应号码的哈希桶中

  • 如果哈希桶中没对象,则直接放入。
  • 如果同一个哈希桶中已有对象,调用该对象equals()与要加入的对象比较结果为false,则表示两个对象非重复对象,可以收集,如果是true,表示两个对象是重复对象,则不予收集。

image-20190621233443753

事实上不只有HashSet,Java中许多要判断对象是否重复时,都会调用hashCode()与equals()方法。

那我们应该怎么修改我们上面的代码呢。

import java.util.HashSet;
import java.util.Set;

class Student2 {
	private String name;
	private String number;

	Student2(String name, String number) {
		this.name = name;
		this.number = number;
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((name == null) ? 0 : name.hashCode());
		result = prime * result + ((number == null) ? 0 : number.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Student2 other = (Student2) obj;
		if (name == null) {
			if (other.name != null)
				return false;
		} else if (!name.equals(other.name))
			return false;
		if (number == null) {
			if (other.number != null)
				return false;
		} else if (!number.equals(other.number))
			return false;
		return true;
	}

	@Override
	public String toString() {
		return String.format("(%s, %s)", name, number);
	}
}

public class Students2 {
	public static void main(String[] args) {
		Set set = new HashSet();
		set.add(new Student2("Justin", "B835031"));
		set.add(new Student2("Monica", "B835032"));
		set.add(new Student2("Justin", "B835031"));
		System.out.println(set);
	}
}

执行结果

[(Monica, B835032), (Justin, B835031)]
1.3.2 TreeSet

TreeSet底层使用TreeMap实现的。所有添加到TreeSet的对象,都需要实现Comparable接口,所以TreeSet是以元素的大小 顺序来进行排序的。一般在我们需要保持元素的顺序的时候使用。

class Student2 implements Comparable {
	private String name;
	private String number;

	Student2(String name, String number) {
		this.name = name;
		this.number = number;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getNumber() {
		return number;
	}

	public void setNumber(String number) {
		this.number = number;
	}

	@Override
	public String toString() {
		return String.format("(%s, %s)", name, number);
	}

	@Override
	public int compareTo(Object o) {
		return this.number.compareTo(((Student2) o).getNumber());
	}
}

public class Students2 {
	public static void main(String[] args) {
		Set set = new TreeSet();
		set.add(new Student2("Justin", "B835031"));
		set.add(new Student2("Monica", "B835030"));
		set.add(new Student2("Jason", "B835032"));

		System.out.println(set);
	}
}

打印如下

[(Monica, B835030), (Justin, B835031), (Jason, B835032)]

1.4 支持队列操作的Queue

如果希望收集对象时可以队列方式

  • 收集的对象加入至尾端
  • 取得对象时可以从前端

则可以使用Queue接口的实现类。

Queue接口常用的方法

  • offer()方法用来在队列后端加入对象,成功会返回true,失败则返回false。
  • poll()方法用来取出队列前端对象,若队列为空则返回null。
  • peek()用来取得(但不取出)队列前端对象,若队列为空则返回null。

下面我们来看一个例子

package com.ripjava.collection;

import java.util.*;

interface Request {
    void execute();
}

public class ReuqestQueue {

    public static void main(String[] args) {
        Queue requests = new LinkedList();
        for (int i = 1; i < 6; i++) {
            requests.offer(new Request() {
                public void execute() {
                    System.out.printf("处理 %f%n", Math.random());
                }
            });
        }
        process(requests);
    }
    
    private static void process(Queue requests) {
        while(requests.peek() != null) {
            Request request = (Request) requests.poll();
            request.execute();
        }
    }
}

1.5 遍历对象

如果要写个forEach()方法,可以显示List收集的所有对象,也许你会这么写:

    private static void forEach( List list) {
        for(int i = 0; i < list.size() ; i++) {
            System.out.println(list.get(i));
        }
    }

无论List、Set还是Queue,都会有个iterator()方法.

我们也可以这样iterator进行遍历。

	private static void forEach(Iterable iterable) {
		Iterator iterator = iterable.iterator();
		while (iterator.hasNext()) {
			System.out.println(iterator.next());
		}
	}

在JDK5之后,实现java.util.Iterable接口的,我们都可以使用增强型for循环进行迭代。

private static void forEach(Iterable iterable) {
  for(Object o : iterable) {
    System.out.println(o);
  }
}

在JDK 8之后,我们可以使用lambda,函数式方式来遍历(了解,以后回讲)。

private static void forEachFunction(Collection collection) {
  collection.forEach(System.out::println);
}

1.6 排序

在收集对象之后,对对象进行排序是常用的动作,你不用亲自操作排序算法,java.util.Collections提供有sort()方法。由于必须有索引才能进行排序,因此Collections的sort()方法接受List操作对象。

比如

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class Sort {
	public static void main(String[] args) {
		List numbers = Arrays.asList(10, 2, 3, 1, 9, 15, 4);
		Collections.sort(numbers);
		System.out.println(numbers);
	}
}

打印结果

[1, 2, 3, 4, 9, 10, 15]

竟然可以给我排好。从小到大的顺序。

执行结果将会很奇怪地抛出ClassCastException?

那我们给我们自己创建的类的对象的集合排序呢?

1.6.1 使用Comparable

是因为你根本没告诉Collections的sort()方法,到底要根据Account的name、number或balance进行排序,那它要怎么排?Collections的sort()方法要求被排序的对象,必须实现java.lang.Comparable接口,

这个接口有个compareTo()方法必须返回大于0、等于0或小于0的数,作用为何?

直接来看如何针对账户余额进行排序就可以了解:

package com.ripjava.collection;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

class Account2 implements Comparable {
    private String name;
    private String number;
    private int balance;

    Account2(String name, String number, int balance) {
        this.name = name;
        this.number = number;
        this.balance = balance;
    }

    @Override
    public String toString() {
        return String.format("Account2(%s, %s, %d)", name, number, balance);
    }

    @Override
    public int compareTo(Object o) {
        Account2 other = (Account2) o;
        return this.balance - other.balance;
    }
}

public class Sort3 {
    public static void main(String[] args) {
        List accounts = Arrays.asList(
                new Account2("Justin", "X1234", 1000),
                new Account2("Monica", "X5678", 500),
                new Account2("Irene", "X2468", 200)
        );
        Collections.sort(accounts);
        System.out.println(accounts);
    }
}

Collections的sort()方法在取得a对象与b对象进行比较时,会先将a对象扮演(Cast)为Comparable(也因此若对象没操作Comparable,将会抛出ClassCastException),然后调用a.compareTo(b),如果a对象顺序上小于b对象,必须返回小于0的值,若顺序上相等则返回0,若顺序上a大于b则返回大于0的值。因此,上面的代码,将会依余额从小到大排列账户对象:

[Account2(Irene, X2468, 200), Account2(Monica, X5678, 500), Account2(Justin, X1234, 1000)]

为何前面的Sort类中,可以直接对Integer进行排序呢?若查看API文件,将可以发现Integer就有操作Comparable接口。

1.6.2 使用Comparator

实际开发总是不断有意外,如果你的对象无法操作Comparable呢?也许你拿不到原始码,也许你不能修改原始码。举个例子来说,String本身有操作Comparable,

package com.ripjava.collection;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class Sort4 {
    public static void main(String[] args) {
        List words = Arrays.asList("B", "X", "A", "M", "F", "W", "O");
        Collections.sort(words);
        System.out.println(words);
    }
}

结果如下

[A, B, F, M, O, W, X]

如果今天你想要让排序结果反过来呢?修改String.java?这个方法不可行吧。

继承String后再重新定义compareTo()方法?也不可能,因为String声明为final,不能被继承。

Collections的sort()方法有另一个重载版本,可接受java.util.Comparator接口的操作对象,如果使用这个版本,排序方式将根据Comparator的compare()定义来决定。例如:

package com.ripjava.collection;

import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

class StringComparator implements Comparator {
    @Override
    public int compare(Object o1, Object o2) {
        String str1 = (String) o1;
        String str2 = (String) o2;
        return -str1.compareTo(str2);
    }
}

public class Sort5 {
    public static void main(String[] args) {
        List words = Arrays.asList("B", "X", "A", "M", "F", "W", "O");
        Collections.sort(words, new StringComparator());
        System.out.println(words);
    }
}

Comparator的compare()会传入两个对象,如果o1顺序上小于o2,必须返回小于0的值,顺序相等则返回0,顺序上o1大于o2则返回大于0的值。在这个范例中,由于String本身就是Comparable,所以将compareTo()返回的值乘上-1,就可以调换排列顺序。执行结果如下:

[X, W, O, M, F, B, A]

1.7 使用泛型

在使用Collection收集对象时,由于事先不知道被收集对象的形态,因此内部操作时,都是使用Object来参考被收集的对象,取回对象时也是以Object类型返回。

由于取回对象时会以Object类型返回,若想针对某类定义的行为操作时,我们就需要强制转型。

List list = Arrays.asList("a1", "a2", "a3");
String word = (String)list.get(0);

Collection虽然可以收集各种对象,但实际上通常Collection中会收集同一种类型的对象,例如都是收集字符串对象。因此从JDK5之后,新增了泛型(Generics)语法,让你在设计API时可以指定类或方法支持泛型,而使用API的客户端在语法上会更为简洁,并得到编译时期检查。

那我们就来看那一下 如何使用泛型。

我们把我们之前写的SimpleArrayList加上泛型。

package com.ripjava.collection;

import java.util.Arrays;

public class SimpleArrayList2<E> {
	
	/*
	 * 数组默认初始化长度
	 */
	private static final int DEFAULT_CAPACITY = 10;
    private Object[] list;
    private int next;
    
    public SimpleArrayList2() {
        this(DEFAULT_CAPACITY);
    }
   
    public SimpleArrayList2(int capacity) {
        list = new Object[capacity];
    }
    
    public void add(E o) {
        if(next == list.length) {
            list = Arrays.copyOf(list, list.length * 2);
        }
        list[next++] = o;
    }
    
    public E get(int index) {
        return (E)list[index];
    }
    
    public int size() {
        return next;
    }
    
    public static void main(String[] args) {
    	SimpleArrayList2<String> testlist = new SimpleArrayList2<String>();
    	testlist.add("b1");
    	//testlist.add(122);
    	// new后边的尖括号可以不指定具体类型。JDK 7之后
    	SimpleArrayList2<String> testlist2 = new SimpleArrayList2<>();
    	
	}
    
}

我们来分析一下

  • 尖括号, 表示此类支持泛型,E只是一个类型代号(表示Element),高兴的话,可以用T、K、V等代号。

    由于使用定义类型,在需要编译程序检查类型的地方,都可以使用E,像是add()方法必须检查传入的对象类型是E,get()方法必须转换为E类型

  • 声明与建立对象时,可使用角括号告知编译程序,这个对象泛型实际使用的是那种对象。

还记得之前 使用的Comparator接口,他也是支持泛型的。

public interface Comparator<T> {
  int compare(T o1, T o2);
}

我们来修改之前使用Comparator的排序。

package com.ripjava.collection;

import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

class StringComparator2 implements Comparator<String> {
    @Override
    public int compare(String s1, String s2) {
        return -s1.compareTo(s2);
    }
}

public class Sort6 {
    public static void main(String[] args) {
        List<String> words = Arrays.asList("B", "X", "A", "M", "F", "W", "O");
        Collections.sort(words, new StringComparator2());
        System.out.println(words);
    }
}

泛型语法可以写的很简单,也可以很复杂。我们就不深入展开了。

练习(20分钟)
  1. 将之前写的SimpleLinkedList添加上泛型。

2 键值对应的Map

就如同网络搜索,根据关键字可找到对应的数据,程序设计中也常有这类需求,根据某个键(Key)来取得对应的值(Value)。可以事先利用java.util.Map接口的操作对象来建立键值对应数据,之后若要取得值,只要用对应的键就可以迅速取得。

2.1 常用Map操作类

先了解Map设计架构,对使用API会有帮助。

image-20190622070122394

常用的Map操作类为java.util.HashMap与java.util.TreeMap,其继承自抽象类java.util.AbstractMap。

至于java.util.Dictionary与java.util.HashTable是从JDK1.0就遗留下来的API,不建议使用。但其子类java.util.Properties倒是还蛮常使用。

2.1.1 使用HashMap

Map也支持泛型语法,使用上很简单,直接来看个使用HashMap的范例,可以根据指定的用户名称取得对应的信息:

package com.ripjava.map;

import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;

public class Messages {
    public static void main(String[] args) {
        Map<String, String> messages = new HashMap<>(); 
        messages.put("Justin", "hello world");
        messages.put("Monica", "to Monica!");
        messages.put("Irene", "Irene  aaaa!");
        
        Scanner scanner = new Scanner(System.in);
        System.out.print("获取谁信息:");
        String message = messages.get(scanner.nextLine());
        System.out.println(message);
        System.out.println(messages);
        scanner.close();
    }
}

分析一下代码

  • 建立Map操作对象时,可以使用泛型语法指定键与值的类型。在这里键使用String,值也使用String类型
  • 要建立键值对应,可以使用put()方法,第一个参数是键,第二个参数是值。
  • 对于Map而言,键不会重复,判断键是否重复是根据hashCode()与equals(),所以作为键的对象必须操作hashCode()与equals()。
  • 若要指定键取回对应的值,则使用get()方法。

在HashMap中建立键值对应之后,键是无序的,这可以在执行结果中看到。如果想让键是有序的,则可以使用TreeMap。

2.1.2 使用TreeMap

如果使用TreeMap建立键值对应,则键的部分将会排序,满足下面条件之一。

  • 作为键的对象必须操作Comparable接口
  • 在创建TreeMap时指定操作Comparator接口的对象。

由于String有操作Comparable接口,因此可看到结果会是根据键来排序:

{Irene=Irene  aaaa!, Justin=hello world, Monica=to Monica!}

假设想看到相反的排序结果,那么可以这样使用Comparator:

package com.ripjava.map;

import java.util.Comparator;
import java.util.Map;
import java.util.TreeMap;

class StringComparator implements Comparator<String> {
    @Override
    public int compare(String s1, String s2) {
        return -s1.compareTo(s2);
    }
}

public class Messages3 {
    public static void main(String[] args) {
        Map<String, String> messages = new TreeMap<>(new StringComparator()); 
        messages.put("Justin", "hello world");
        messages.put("Monica", "to Monica!");
        messages.put("Irene", "Irene  aaaa!");
        System.out.println(messages);
    }
}

3.使用Properties

Properties类继承自HashTable,HashTable操作了Map接口,Properties自然也有Map的行为。虽然也可以使用put()设定键值对应、get()方法指定键取回值,不过一般常用Properties的setProperty()指定字符串类型的键值,getProperty()指定字符串类型的键,取回字符串类型的值,通常称为属性名称与属性值。

比如

import java.util.Properties;

public class PropertiesDemo {
    public static void main(String[] args) {
        Properties props = new Properties();
        props.setProperty("username", "justin");
        props.setProperty("password", "123456");
        System.out.println(props.getProperty("username"));
        System.out.println(props.getProperty("password"));
    }
}

Properties也可以从文档中读取属性,我们先建一个.properties文档如下:

package com.ripjava.map;

import java.io.IOException;
import java.util.Properties;

public class LoadProperties {
    public static void main(String[] args) throws IOException {
        Properties props = new Properties();
        props.load(LoadProperties.class.getResourceAsStream(args[0]));

        System.out.println(props.getProperty("com.ripjava.username"));
        System.out.println(props.getProperty("com.ripjava.password"));
    }
}

.properties的=左边设定属性名称,右边设定属性值。可以使用Properties的load()方法指定InputStream的实例,从文档中加载属性。

load()方法结束后,会自动关闭InputStream实例。

在使用java指令启动JVM时,可以使用-D指定系统属性。

可以使用System的static方法getProperties()取得Properties实例,该实例包括了系统属性。

package com.ripjava.map;

import java.util.Properties;

public class LoadSystemProps {
    public static void main(String[] args) {
        Properties props = System.getProperties();
        System.out.println(props.getProperty("username"));
        System.out.println(props.getProperty("password"));
    }
}

我们需要在JVM启动时候,指定对应参数。

-Dusername=test -Dpassword=qweqweqw

2.2 访问Map键值

如果想取得Map中所有的键,或是想取得Map中所有的值该怎么做?

  • 如果想取得Map中所有的键,可以调用Map的keySet()返回Set对象。由于键是不重复的,所以用Set操作返回是理所当然的做法
  • 如果想取得Map中所有的值,则可以使用values()返回Collection对象
package com.ripjava.map;

import java.util.HashMap;
import java.util.Map;

public class MapKeyValue {
    public static void main(String[] args) {
        Map<String, String> map = new HashMap<>();
        map.put("one", "一");
        map.put("two", "二");
        map.put("three", "三");
        
        System.out.println("key值");
        foreach(map.keySet());
        
        System.out.println("value值");
        foreach(map.values());
    }
    
    public static void foreach(Iterable<String> iterable) {
        for(String element : iterable) {
            System.out.println(element);
        }
    }
}

如果想同时取得Map的键与值,可以使用entrySet()方法,这会返回一个Set对象,每个元素都是Map.Entry实例,可以调用getKey()取得键,调用getValue()取得值。

package com.ripjava.map;

import java.util.Map;
import java.util.TreeMap;

public class MapKeyValue2 {
    public static void main(String[] args) {
        Map<String, String> map = new TreeMap<>();
        map.put("one", "一");
        map.put("two", "二");
        map.put("three", "三");
        foreach(map.entrySet());
    }
    
    public static void foreach(Iterable<Map.Entry<String, String>> iterable) {
        for(Map.Entry<String, String> entry: iterable) {
            System.out.printf("(Key %s, Value %s)%n", 
                    entry.getKey(), entry.getValue());
        }
    }
}

最后,问一下 大家,在Map里我可以不用泛型吗?

import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

public class MapKeyValue3 {
    public static void main(String[] args) {
        Map map = new TreeMap();
        map.put("one", "一");
        map.put("two", "二");
        map.put("three", "三");
        
        Set entries = map.entrySet();
        for(Object o: entries) {
            Map.Entry entry = (Map.Entry) o;
            String key = (String) entry.getKey();
            String value = (String) entry.getValue();
            System.out.printf("(鍵 %s, 值 %s)%n", key, value);
        }
    }
}

答案是可以的,但是不建议大家这么做。