关闭

Java学习笔记--Collection与Map

标签: javaCollectionMap
403人阅读 评论(0) 收藏 举报
分类:

使用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语法之后会学习到,到时候在进行总结。

1
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:60867次
    • 积分:1634
    • 等级:
    • 排名:千里之外
    • 原创:86篇
    • 转载:12篇
    • 译文:0篇
    • 评论:76条
    博客专栏
    最新评论