天天看点

CS61B sp2018笔记 | Generics and Autoboxing

1. Automatic Conversions

1.1 Autoboxing and Unboxing

Java中的泛型用到了

<>

,当我们实例化一个泛型的时候,必须要指明一个确定的类型。

回忆一下,Java有8中基本类型,其他所有的类型都是引用类型。Java的一个特点就是不能把基础类型当作参数传递给泛型,比如说,

ArrayDeque<int>

是一个语法错误,正确的写法是

ArrayDeque<Integer>

对于每一种基本类型,都有一种引用类型与之对应,这些引用类型称为

包装类(wrapper classes)

CS61B sp2018笔记 | Generics and Autoboxing

我们假定基本类型和包装类之间没有类型转换,这时如果我们要使用一个泛型的数据结构,就不得不这样做:

public class BasicArrayList {
    public static void main(String[] args) {
      ArrayList<Integer> L = new ArrayList<Integer>();
      L.add(new Integer());
      L.add(new Integer());

      /* Use the Integer.valueOf method to convert to int */
      int first = L.get().valueOf();
    }
}
           

幸运的是,Java为基本类型和包装类之间提供了隐式的转换,所以我们可以写出这样的代码:

public class BasicArrayList {
    public static void main(String[] args) {
      ArrayList<Integer> L = new ArrayList<Integer>();
      L.add();
      L.add();
      int first = L.get();
    }
}
           

这种转换称为

box

unbox

,也就是

Auto-box(自动装箱功能)

对自动装箱功能有几个需要注意的地方:

  • 数组没有自动装箱功能,比如,你有一个整数类型的数组

    int[] x

    ,它将不会自动转换成

    Integer[]

    类型。
  • 自动装箱功能会减慢程序运行的速度。
  • 包装类会占据更大的内存空间。

1.2 Widening

还有一种自动转换是Java基本类型之间的转换,内存小的类型会自动转换成内存大的类型,比如说我们有这样一个函数:

public static void blahDouble(double x) {
    System.out.println(“double: “ + x);
}
           

我们可以这样调用它:

int x = 20;
blahDouble(x);
           

不过要把内存大的类型转换成内存小的类型,就必须进行强制类型转换:

public static void blahInt(int x) {
    System.out.println(“int: “ + x);
}
           

我们需要这样调用它:

double x = ;
blahInt((int) x);
           

2. Immutability

数据类型分为

可变型(mutable)

不变型(immutable)

一个不变的数据类型它的

实例

任何情况下都不会发生改变。比如说,Java中的

String

类型就是不变的,一旦字符串被确定,它就再也不会改变,即使想对它进行一些改变,也要返回一个新的字符串。

数据类型的不变性使用

final

关键字定义,比如说下面这个Date类:

public class Date {
    public final int month;
    public final int day;
    public final int year;
    private boolean contrived = true;
    public Date(int m, int d, int y) {
        month = m; day = d; year = y;
    }
}
           

当实例化一个

Date

类后,就无法再改变它的属性了。

我们再来看一个特殊的例子:

public final ArrayDeque<String>() deque = new ArrayDeque<String>();
           

变量deque被定义为final代表它不会进行再次分配,但是我们知道deque里面存储的是引用,引用不能改变,但是引用所指向内存中的内容是可以改变的,所以说

ArrayDeque永远都是可以改变的

3. Generics

3.1 Creating Another Generic Class

在此之前,我们已经创建了一些泛型链表,下面让我们创建一种新的数据类型——

maps

我们将建立

ArrayMap

类,它实现

Map61B

接口,这个接口目前有以下几个基本方法:

put(key, value): Associate key with value.
 containsKey(key): Checks if map contains the key.
 get(key): Returns value, assuming key exists.
 keys(): Returns a list of all keys.
 size(): Returns number of keys.
           

为了快速建立模型,我们忽略内存的重新分配问题,需要说明一下,我们的类中

一个key只能对应一个value

,下面是它的实现:

package Map61B;

import java.util.List;
import java.util.ArrayList;

/***
 * An array-based implementation of Map61B.
 ***/
public class ArrayMap<K, V> implements Map61B<K, V> {

    private K[] keys;
    private V[] values;
    int size;

    public ArrayMap() {
        keys = (K[]) new Object[];
        values = (V[]) new Object[];
        size = ;
    }

    /**
    * Returns the index of the key, if it exists. Otherwise returns -1.
    **/
    private int keyIndex(K key) {
        for (int i = ; i < size; i++) {
            if (keys[i].equals(key)) {
            return i;
        }
        return -;
    }

    public boolean containsKey(K key) {
        int index = keyIndex(key);
        return index > -;
    }

    public void put(K key, V value) {
        int index = keyIndex(key);
        if (index == -) {
            keys[size] = key;
            values[size] = value;
            size += ;
        } else {
            values[index] = value;
        }
    }

    public V get(K key) {
        int index = keyIndex(key);
        return values[index];
    }

    public int size() {
        return size;
    }

    public List<K> keys() {
        List<K> keyList = new ArrayList<>();
        for (int i = ; i != size; i++) {
            keyList.add(keys[i]);
        }
        return keyList;
    }
}
           

这个map基于array数组,它不是最好的,但是可以用来说明一些问题。

我们写一个测试类来测试一下:

@Test
public void test() { 
    ArrayMap<Integer, Integer> am = new ArrayMap<Integer, Integer>();
    am.put(, );
    int expected = ;
    assertEquals(expected, am.get());
}
           

你将会发现报了一个运行时错误:

$ javac ArrayMapTest.java ArrayMapTest.java:11: error: reference to assertEquals is ambiguous assertEquals(expected, am.get(2)); ^ both method assertEquals(long, long) in Assert and method assertEquals(Object, Object) in Assert match

错误的原因是JUnit的

assertEquals

方法重载了,我们只需要把最后一行代码改为

assertEquals((Integer)expected, am.get(2));

,明确调用的是

assertEquals(Object, Object)

方法即可。

3.2 Generic Methods

接下来我们将建立一个新的类

MapHelper

,它有以下两个方法:

  • get(Map61B, key):根据给出的key值返回Map61B中对应的value,如果key不存在的话,返回null
  • maxKey(Map61B):返回Map61B中key的最大值。

3.2.1 Implementing get

在这个类中,我们没有在类名的后边声明这个类是一个泛型,而是在动议每个方法时声明,所以get方法就会是这样:

public static <K,V> V get(Map61B<K,V> map, K key) {
    if map.containsKey(key) {
        return map.get(key);
    }
    return null;
}
           

我们可以这样使用它:

ArrayMap<Integer, String> isMap = new ArrayMap<Integer, String>();
System.out.println(mapHelper.get(isMap, ));
           

3.2.1 Implementing maxKey

一开始实现这个方法的时候你可能会这样写:

public static <K, V> K maxKey(Map61B<K, V> map) {
    List<K> keylist = map.keys();
    K largest = map.get();
    for (K k: keylist) {
        if (k > largest) {
            largest = k;
        }
    }
    return largest;
}
           

不过很快你将发现一些问题,K类型之间的比较发生了错误,这就意味着我们要考虑对象的比较规则,所以要实现

Comparable

接口:

public static <K extends Comparable<K>, V> K maxKey(Map61B<K, V> map) {
    List<K> keylist = map.keys();
    K largest = map.get();
    for (K k: keylist) {
        if (k.compareTo(largest)) {
            largest = k;
        }
    }
    return largest;
}
           

可能你对这个函数的定义有些困惑,特别是返回类型

<K extends Comparable<K>, V> K

这一部分。

首先

Comparable

接口本身就是一个范式的接口,所以我们必须指明我们要比较的类型,也就是在Comparable后面加

<K>

那么为什么不用

implements

反而使用

extends

呢?其实,这里的extends有了不一样的含义。当我们说

K extends Comparable

时,仅仅意味着K必须可以比较,extends不同种的使用方法称为

type upper bounding

继续阅读