使用Collection收集对象
Collection架构
在Java SE中,我们有各种各样的API可以使用,数量庞大,种类繁杂,我们要熟用这些API,就需要去了解他们的继承与接口操作,知道在何时该用哪个类,而不用死背API。为此,我们必须熟悉他们的接口继承架构设计。所以在学习前,我们来看一下Collection的继承接口架构:
从这个图片中,我们可以很清晰的看出每个类与接口的继承关系,哪个类调用了哪个接口。对于这张图中的接口,接下来我会给大家一一介绍。
注:图中的实线表示继承,虚线表示操作这个接口,凡是没有标示interface的,都表示的是类。
具有索引的List
List是一种Collection,由这句话可以知道List是继承了Collection接口的,它的作用是收集对象。从上图中我们可以看到,操作List的类有两个,一个是ArrayList,另一个是LinkedList。当我们需要以索引方式保留收集的对象顺序,就需要操作这个接口。
List接口定义了add(),remove(),set()等许多依索引操作的方法。那么我们刚也说过,操作List接口的类有两个,我们什么时候用ArrayList,什么时候用LinkedList呢?
- ArrayList: ArrayList操作时内部就是用Object数组来保存收集的对象,既然是数组,那么他就拥有数组的优势与劣势,对于查找和排序来说,他是非常方便的。一旦涉及到了删除,添加元素,那么他就没有链表来的方便,并且数组的长度问题我们也是需要关注的,一旦ArrayList内部数组的长度不够的时候,就会建立新的数组,并将旧的数组参考指定给新数组,这肯定是浪费时间与内存的。好在的是ArrayList类提供了一个构造函数,可以让我们指定这个数组的容量。
- LinkedList: 对于这个类,和C语言中链表的构造方法是很类似的,我们可以来看一下构造这个类的方法,它拥有链表的优势,适合删除,添加元素,并且不会浪费内存空间。
/**
* Created by paranoid on 16-12-8.
*/
public class SimpleLinkedList {
private class Node{ //将收集到的对象用Node进行封装
Node(Object o){
this.o = o;
}
Object o;
Node next;
}
private Node first; //指向第一个节点
public void add(Object elem){ //添加新的Node封装对象
Node node = new Node(elem);
if(first == null){
first = node;
}
else{
append(node);
}
}
private void append(Node node){
Node last = first;
while(last.next != null){
last = last.next;
}
last.next = node;
}
public int size(){
int count = 0;
Node last = first;
while(last != null){
last = last.next;
count++;
}
return count;
}
public Object get(int index){
checkSize(index);
return findElemOf(index);
}
private void checkSize(int index) throws IndexOutOfBoundsException{
int size = size();
if(index >= size){
throw new IndexOutOfBoundsException(
String.format("Index: %d, Size: %d", index, size));
}
}
private Object findElemOf(int index){ //访问所有的Node并计数以取得对应的索引对象
int count = 0;
Node last = first;
while(count < index){
last = last.next;
count++;
}
return last;
}
}
我们可以看到,它的构造过程和链表是十分类似的。
内容不重复的Set
当我们收集对象的时候,需要收集的对象之中不能有重复的对象,当我们有这个需求的时候,就可以使用Set接口。我们在上面那个图中可以看到,操作这个接口的类有两个,分别是HashSet和TreeSet。我们先来介绍HashSet:
import java.util.*;
/**
* Created by paranoid on 16-12-8.
*/
public class WordCount {
public static void main(String[] args){
Scanner console = new Scanner(System.in);
System.out.print("请输入英文:");
Set words = tokenSet(console.nextLine());
System.out.printf("不重复的单词有%d 个: %s\n", words.size(), words);
}
static Set tokenSet(String line){
String[] tokens = line.split(" "); //根据空白切割出字符串
return new HashSet(Arrays.asList(tokens));
}
}
运行结果:
因为Arrays.asList()方法返回List,而List是一种Collection,因此它可以传给HashSet,最后得到正确的结果。
但是我们有时候单单使用这个方法的话是会出现失败的,如果我们收集的是对象,然后我们还没有告诉Set什么样的实例才算是重复,那么程序的运行结果就有可能和我们的预期是有偏差的,对于HashSet怎么来判断对象相同,这是由它内部的hashcode和equals方法进行的。刚开始我也没把这部分内容没有弄明白,所以在网上找了一些资料,如果大家也不懂得话,我转载了一篇博客,名字叫做《什么时候需要重写equals方法?为什么重写equals方法,一定要重写HashCode方法?》大家可以看看。在这里我直接上代码:
/**
* Created by paranoid on 16-12-8.
*/
import java.util.*;
class Student2 {
private String name;
private String number;
Student2(String name, String number){
this.name = name;
this.number = number;
}
@Override
public int hashCode(){
//Object有hash方法可以用
//以下可以简化为return Objects.hash(name, number);
int hash = 7;
hash = 47 * hash + Objects.hashCode(this.name);
hash = 47 * hash + Objects.hashCode(this.number);
return hash;
}
@Override
public boolean equals(Object obj){
if(obj == null){
return false;
}
if(getClass() != obj.getClass()){
return false;
}
final Student2 other = (Student2) obj;
if(!Objects.equals(this.name, other.name)){
return false;
}
if(!Objects.equals(this.number, 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 students = new HashSet();
students.add(new Student2("justin", "B835031"));
students.add(new Student2("Monica", "B835032"));
students.add(new Student2("justin", "B835031"));
System.out.println(students);
}
}
支持队列操作的Queue
如果我们希望收集对象的时候是以队列的方式进行收集,那么我们可以操作Queue接口。Queue继承自Collection,所以也有add(),remove(),element()等方法,然而Queue定义了offer,poll,peek等方法,他们之间的差别最主要的在于前三个方法操作失败时会抛出异常,但是后三个方法操作失败时会返回特定的值。
- offer(): 在队列后端加入对象;
- poll(): 在队列前端取出对象;
- peek(): 取得队列前端的对象,但不取得。
前面提到的LinkedList不仅操作了List接口,也操作了Queue的行为。,具体代码我就不在这里陈列了。我们来看一下Queue的子接口Deque。
Deque允许在队列的两端进行操作,我们来看一个使用Deque来操作容量有限的堆栈:
/**
* Created by paranoid on 16-12-8.
*/
import java.util.*;
import static java.lang.System.out;
public class Stack {
private Deque elems = new ArrayDeque();
private int capacity;
public Stack(int capacity){
this.capacity = capacity;
}
public boolean push(Object elem){
if(isFull()){ //判断栈是否已满
return false;
}
return elems.offerLast(elem);
}
private boolean isFull(){
return elems.size() + 1 > capacity;
}
public Object pop(){
return elems.pollLast();
}
public Object peek(){
return elems.peekLast();
}
public static void main(String[] args){
Stack stack = new Stack(5);
stack.push("Justin");
stack.push("Monica");
stack.push("Irene");
out.println(stack.pop());
out.println(stack.pop());
out.println(stack.pop());
}
}
运行结果:
由于栈是先进后出,所以最先进去的Justin最后显示出来。
使用泛型
在使用Collection收集对象时,由于事先不知道收集对象的类型,因此内部操作时,都是使用Object来收集被参考的对象,取回对象时也是以Object类型返回,所以如果我们针对某类定义的行为操作时,就必须告诉编译程序,让对象重新扮演该类型。例如:
List names = Arrays.asList("Justin", "Monica", "Irene");
String name = (String) names.get(0);
Collection收集对象时,考虑到收集各种对象的需求,因此内部操作采用Object参考收集的对象,这会让执行时期被收集的对象失去形态信息,因此取回对象之后,必须自行记得对象的真正类型。
虽然Collection被用于收集各种对象,但实际上Collection通常会收集同一种类型的对象,例如都是手机字符串的对象。这时候,我们就可以使用泛型语法。
/**
* Created by paranoid on 16-12-8.
*/
import java.util.Arrays;
public class ArrayList <String>{
private Object[] elems;
private int next;
public ArrayList(int capacity){
elems = new Object[capacity];
}
public ArrayList(){
this(16);
}
public void add(String e){
if(next == elems.length){
elems = Arrays.copyOf(elems, elems.length * 2);
}
elems[next++] = e;
}
public String get(int index){
return (String) elems[index];
}
public int size(){
return next;
}
}
从上面的代码我们可以看到,在类名称的旁边出现了 string,这表示此类型支持泛型,实际加入ArrayList的对象会是客户端声明的String,当然也可以是其他类型。之后我们如果调用这个类,就可以这样进行:
ArrayList<String> names = new ArrayList<>(); //JDK7之后的写法
names.add("Justin");
String name = names.get(0);
接口也可以使用泛型,使用方法同上。
Interable与Iterator
从第一张图我们可以看到,Collection继承自Interable。在JDK5中有一个方法是定义在Collection之中的,这个方法就是Iterator,它会返回Java.util.Iterator接口的操作对象,这个对象包括了Collection收集的所有对象,我们可以先使用Iterator的hasNext方法看看有无下一个对象,若有的话再使用next方法取得下一个方法。由它来显示Collection所收集的对象,具体代码如下:
static void forEach(Collection collection){
Iterator iterator = collection.iterator();
while(iterator.hasNext()){
out.println(iterator.next());
}
}
上面的写法也是可以的,但是在JDK5之后,原先定义在Collection中的iterator方法,提升值新的Interable接口,所以上面的代码只需进行简单的修改就可以使用,在这里我就不再赘述。
在Interable接口中,我们还可以使用增强式for循环来使上面代码更加简洁:
static void forEach(Iterable iterable){
for(Object o : iterable){
System.out.println("o");
}
}
Comparable与Comparator
当我们收集到对象之后最常做的就是排序工作了吧,在Collections中提供有sort方法,但是一旦收集的对象中包含好几种类型之后,排序就不知道该按对象之中的哪种数据类型进行排序,这时候我们就要操作Comparable接口,这个接口有一个compareTo方法可以让我们指定由这个对象中的哪个数据类型来进行排序。我们来看一个例子:
/**
* Created by paranoid on 16-12-8.
*/
import java.util.*;
class Account2 implements Comparable<Account2>{
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(Account2 other){
return 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);
}
}
在这个程序中,我们按照资金的大小进行了升序排序。
意外总是不断的发生,我们有可能在编写程序的时候出现这种情况:我们操作不了Comparable接口,也许我们拿不到原始码,也许我们不能修改原始码,举个例子,在String类中,本身就有操作Comparable,所以可以进行排序,然而如果我们突然想要进行降序排序,我们可以操作CompareTo方法吗?不能,因为String已经被声明为final,不能被继承,也就不能进行方法的重新定义。这个时候,我们可以使用Comparator接口,然后我们来重新定义其中的compare方法。具体代码如下:
/**
* Created by paranoid on 16-12-10.
*/
import java.util.*;
class StringComparator implements Comparator<String>{
@Override
public int compare(String s1, String s2){
return -s1.compareTo(s2);
}
}
public class sort {
public static void main(String[] args){
List<String> words = Arrays.asList("A", "B", "C", "D", "E");
Collections.sort(words ,new StringComparator());
System.out.println(words);
}
}
例如上面的代码,我们在StringComparator这个勒种操作了Comparator这个接口,并且将compare方法进行了重新定义,由于s1,s2都是String,所以他们可以操作compareTo方法,将其返回值乘上-1,就是我们要的结果。
键值对应的Map
常用的Map操作类
首先既然我们要使用Map相关的API,那么我们就先来了解一下它的设计架构。
上图中,实线表示继承,虚线表示操作接口。
使用HashMap
在使用Map的时候,也可以使用泛型语法。来看个实例吧:
/**
* Created by paranoid on 16-12-10.
*/
import java.util.*;
import static java.lang.System.out;
public class Messages {
public static void main(String[] args){
Map<String, String> messages = new HashMap<>();
messages.put("Justin", "Hello! Justin的信息"); //建立键值对应
messages.put("Monica", "给Monica的悄悄话");
messages.put("Irene", "Irene的可爱猫喵喵叫");
Scanner console = new Scanner(System.in);
out.println("请输入要取得的信息:");
String message = messages.get(console.nextLine()); //指定键的返回值
out.println(message);
out.println(messages);
}
}
从上面的代码中我们可以很清楚的看到,当我们需要建立键值对应的时候,我们使用的是Map的put方法,对于Map而言,键不会重复,判断键是否会重复是根据HashCode和Equals,所以要成为键,这个对象必须操作HashCode和Equals。当我们要取回一个键对应的值,就是用Map的get方法就行了。看一下运行结果吧:
使用TreeMap
如果使用TreeMap,则键的部分会被排序,条件是作为键的对象必须操作Comparable接口,或者是在 创建TreeMap时指定操作Comparator接口的对象。这个的使用方法和上面的HashMap大同小异,我就不贴出代码了。
使用Properties
这个东西继承自HashTable,而HashTable操作了Map接口,所以Properties自然也拥有Map的行为。这个类更多的是用于字符串。setProperty指定字符串的键值,getProperty根据所指定的键取回对应的值。使用方法和上面的都一样,就不在赘述了。
访问Map键值
当我们想要取得Map中所有的键的时候,我们应该怎么办,虽然Map和Collection没有继承上的关系,但却是彼此搭配的API。由于键是不重复的,所以我们可以调用KeySet方法返回Set对象,这是理所当然的。如果想要返回值,那我们需要使用values()返回Collection对象,来看一个具体操作的代码:
/**
* Created by paranoid on 16-12-10.
*/
import java.util.*;
import static java.lang.System.out;
public class MapKeyValue {
public static void main(String[] args){
Map<String, String> map = new HashMap<>();
map.put("one", "1");
map.put("two", "2");
map.put("three", "3");
out.println("显示键");
map.keySet().forEach(key -> out.println(key)); //Lambda语法
out.println("显示值");
map.values().forEach(key -> out.println(key));
}
}
对于上面代码中的Lambda语法之后会学习到,到时候在进行总结。