6 分钟阅读

原文见 Josh Hug 为 Berkeley CS61B 编写的教材

最新版本

Essentials

Static Typing

在 Java 中,每个变量和表达式都有一个 static type 变量可以且只可以包含该类型的值。另外,变量的类型永远不会改变。

Objects

Static Methods

Java 中的所有代码都必须是类(或类似的东西)的一部分,大部分代码都是在 methods 内部编写的。

Instance Variables and Objects Instantiation

类可以进行实例化,实例可以保存数据。注意:

  • Java 中的 Object 是任何类的实例;
  • 类中自己的变量,也称实例变量或非静态变量,必须在类中声明;
  • 在类中创建的方法没有关键词,我们将这类方法称为实例方法或非静态方法;
  • 要调用该方法,我们必须先使用关键词 new 进行实例化;
  • 一旦对象被实例化,就可以将其分配给适当类型的声明变量;
  • 类的变量和方法也称为类的成员;
  • 类 Bird 的成员使用 Bird.xxx 进行访问;

对象、数组与类等的实例化在电脑操作中表现为创建一个 64 位的指针指向实例化的地址。

Constructors in Java

我们通常使用构造函数在面向对象语言中构造对象,我们只需向类中加一个”constructor”

public class Bird {
    public int beauty;

    //constructor
    public Bird(int w) {
        beauty = b;
    }

    public void sing() {
        if (beauty < 10) {
            System.out.println("yipyipyip!");
        } else if (beauty < 30) {
            System.out.println("ye. ye.");
        } else {
            System.out.println("boooooool!");
        }
    }
}

Array Instantiation, Arrays of Objects

数组在 Java 中同样使用 new 关键词进行实例化,实例化对象的数组也是一样。

public class Array {
    public static void main(String[] args) {
        /* Create an array of five integers. */
        int [] ArrayDemo = new int[5];
        ArrayDemo[3] = 2;
        /* Create an array of two birds. */
        Bird[] birds = new Bird[2];
        Bird[0] = new Bird(9);
        Bird[1] = new Bird(28);
    }
}

Class Methods vs. Instance Methods

Java 允许我们定义两种类型的方法:

  • 类方法,静态方法。例如:

    x = Math.sqrt(100);
    
  • 实例方法,非静态方法。例如:

    Math m = new Math();
    x = m.sqrt(100);
    

实例方法是只能由类的特定实例执行的操作,静态方法是类自身执行的操作。

Static Variables

静态变量应使用类的名称而不是特定实例来访问。但事实上,Java 在技术上允许通过实例名称访问静态变量。

public static void main(String[ ] args)

args 是一个用于接受命令行输入的字符串数组,它允许用户在运行程序时传递动态值,从而使 Java 应用能根据不 同输入执行不同操作。

Testing

Ad Hoc Testing

JUnit Testing

org.junit 库提供了许多有用的方法与功能来简化测试的编写。

  • 我们将 import 语句添加到文件的顶部;

    import org.junit.Test;
    import static org.junit.Assert.*
    
  • 然后再每个检测前面加上

    @Test
    

Intro and interfaces

The Problem

重载的缺点:

  • 非常重复且丑陋,因为代码块几乎相同;
  • 需要维护的代码更多;
  • 如果我们希望添加变量类型,则需要写更多的重载函数。

Hypernyms, Hyponyms, and Interface Inheritance

接口涉及到 Java 中的关系层次,最基础的即为上位词与下位词。现在有 SLList 和 AList 两个 Java 列表类,我们希望创建一个更通用的列表类,命名为 List61B,它就是 SLList 和 AList 的上位词,也是 Java 中所说的接口 interface。我们可以注意到,此处接口的内容中实际上只指定了需要做什么,但没有给出具体的实现。

public interface List61B<Item> {
    public void addFirst(Item x);
    public void add Last(Item y);
    public Item getFirst();
    public Item getLast();
    public Item removeLast();
    public Item get(int i);
    public void insert(Item x, int position);
    public int size();
}

然后,我们还需要指定 AList 和 SLList 是 List61B 的下位词,我们使用关键词 implements。实际的含义是:保证下位词将拥有和定义上位词接口中指定的所有属性和方法。

public class SLList<Item> implements List61B<Item>

Overriding

在子类中实现接口中指定的方法时,可以在方法的顶部加入标签

@Override

Interface Inheritance

接口继承指的是子类继承超类的所有方法/行为的关系,这种继承可以是多代的。

Implementation Inheritance

上面的接口中我们没有进行任何方法的实现,倘若在接口中进行方法的实现,则为实现继承,需要包含关键词 defalut,所有实现该超类的子类都可以使用该方法。

default public void print() {
    for (int i = 0; i < size(); i += 1) {
        System.out.print(get(i) + " ");
    }
    System.out.println();
}

同时若我们在 SLList 重新实现这个方法并覆盖,那么当我们在 SLList 上调用该方法,都会调用覆盖后的方法。

@Override
public void print() {
    for (Node p = sentinel.next; p != null; p = p.next) {
        System.out.print(p.item + " ");
    }
}

总结(接口):

  • 所有方法必须是 public;
  • 所有变量必须是 public static final;
  • 无法实例化;
  • 默认情况下,所有方法都是抽象的,除非指定为 default;
  • 可以为每个类实现多个接口。

Extends, Casting, Higher Order Functions

Extends

如果我们想定义类之间的层次结构关系,例如我们想构建一个 RotatingSLList,它具有与 SLList 相同的功能 ,但有一个额外的操作 rotateRight 将最后一项带到列表的前面。因此,我们希望可以继承 SLList 中的代码, 使用关键词 extends

public class RotatingSLList<Item> extends SLList<Item>

现在,extends 让我们保留了 SLList 的基础功能,并可以进行修改和添加其他功能。实际上,通过该关键词,子 类将继承父类的所有成员,包括:

  • 所有实例和静态变量
  • 所有方法
  • 所有嵌套类

需注意的是,构造函数不能继承,private 成员也不能由子类直接访问。但是,Java 要求所有构造函数都必须调用其超类的构造函数之一开始。

The Object Class

Java 中的每个类实际上都是 Object 类或 Object 类的子类。而 Object 类主要提供了每个 Object 都能执行的一些操作。

Encapsulation

封装是我们对抗复杂性的重要手段之一,也是模块化的重要手段。我们首先来讨论继承如何破坏封装。假设我们在 Dog 类中有两个方法如下:

public void bark() {
    System.out.println("bark");
}

public void barkMany(int N) {
    for (int i = 0; i < N; i += 1) {
        bark();
    }
}

或者我们可以这样实现:

public void bark() {
    barkMany(1);
}

public void barkMany(int N) {
    for (int i = 0; i < N; i += 1) {
        System.out.println("bark");
    }
}

从用户角度看,二者的功能是完全相同的。但倘若用户想定义一个子类并覆盖 barkMany 的方法

@Override
public void barkMany(int N) {
    System.out.println("As a dog, I say: ");
    for (int i = 0; i < N; i += 1) {
        bark();
    }
}

程序将进入无限循环。

Type Checking and Casting

强制类型转换强大却十分危险。

Higher Order Functions

高阶函数是将其他函数视为数据的函数,在 Java 中,我们可以利用接口继承来实现。

Subtype Polymorphism vs. HoFs

Subtype Polymorphism

多态性的核心意思是“多种形式”,指对象具有多种形式或类型。假设我们想编写一个 python 程序,它打印两个对象中较大的字符串表示,那么有两种方法可以解决这个问题。

  1. 显式 HoF 方法

    def print_larger(x, y, compare, stringify):
     if compare(x, y):
         return stringify(x)
     return stringify(y)
    
  2. 亚型多态型方法

    def print_larger(x, y):
     if x.largerThan(y):
         return x.str()
     return y.str()
    

假设我们想编写一个函数 Max,该函数接受任何数组(无论类型),并返回数组中的最大项目。这在 python 或者 C++中或许可以通过重新定义运算符的含义实现,但在 Java 中,我们的思路是创建一个接口来保证任何实现类都包含一个比较方法。

public interface OurComparable {
    public int compareTo(Object o);
}

我们这样定义实现:

  • 如果 this < o,则返回-1;
  • 如果 this = o,则返回 0;
  • 如果 this > o,则返回 1。
public class Bird implements OurComparable {
    private String name;
    private int beauty;

    public Bird(String n, int b) {
        name = n;
        beauty = b;
    }

    public void sing() {
        System.out.println(name + " says: wow");
    }

    public int compareTo(Object o) {
        //需强制类型转换为Bird
        Bird littleBird = (Bird) o;
        if (this.beauty < littleBird.beauty) {
            return -1;
        } else if (this.beauty == littleBird.beauty) {
            return 0;
        }
        return 1;
    }
}

那么现在我们可以将函数推广到接受对象,并确保所有对象都实现了该方法。下面的方法返回 OurComparable 类型的对象

public static OurComparable max(OurComparable[] items) {
    int maxDex = 0;
    for (int i = 0; i < items.length; i += 1) {
        int cmp = items[i].compareTo(items[maxDex]);
        if (cmp > 0) {
            maxDex = i;
        }
    }
    return items[maxDex];
}

Libraries, Abstract Classes, Packages

Abstract Data Types (ADTS)

以上文接口部分的代码为例,我们称 List61B 为 Abstract 数据类型。因为它只附带实现而并未以任何具体方式展示这些实现。

Java Libraries

Java 中具有一些内置的 Abstract 数据类型在 Java 库中。java.util 库中有三个最重要的 ADT:

  • List:项的有序集合,常用实现是 ArrayList
  • Set:严格唯一的项的无序集合(无重复),常用实现是 HashSet
  • Map:键值对的集合,可以通过 key 访问该值,常用实现是 HashMap

Abstract classes

抽象类可以理解成介于 interfaces 和 concrete 类之间的新类,总体来说,它可以做所有 interfaces 可以做的,甚至更多。它的特征如下:

  • 方法可以是 public 或者 private;
  • 可以包含任何类型的变量;
  • 无法实例化;
  • 默认情况下,方法是具体的,除非指定为 abstract;
  • 每个类只能实现一个。

Packages

Autoboxing

Industrial Strength Syntax

Autoboxing and Unboxing

Java 只有 8 个基元类型,其他都是引用类型。在 Java 语法中,我们不能提供原始类型作为泛型的实际类型参数,而需要使用相应的 reference type。不过幸运的是,Java 可以在原始类型和包装类型之间进行隐式转换,即会在原始类型和相应引用类型之间”box”和”unbox”值。

Primitive Class
byte Byte
short Short
int Integer
long Long
float Float
double Double
boolean Boolean
char Character

但需要注意的是:

  • 数组是永远不会自动进行 boxing 或者 unboxing 的;
  • box 和 unbox 会对性能产生可衡量的影响,运行速度会变慢;
  • 包装类型使用的内存比基元类型多很多。

Widening

类似的,Java 也会在代码需要时自动短值赋长值进行加宽。

Immutability

不可变数据类型,即其实例在实例化后无法以任何可观察的方式进行改变。任何具有非私有变量的数据类型都是可变的,除非用 final 进行了声明。 但需要注意的是,将引用声明为 final 并不会使引用指向的对象不可变,例如:

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

变量 deque 初始化后,始终指向同一个 ArrayDeque 实例,但可以修改这个 ArrayDeque 的内容,如添加、删除和更改元素。同时使用 Reflection API 甚至可以对私有变量进行更改。

Generics

Creating Another Generic Class

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[100];
        values = (V[]) new Object[100];
        size = 0;
    }

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

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

    public void put(K key, V value) {
        int index = keyIndex(key);
        if (index == -1) {
            keys[size] = key;
            values[size] = value;
            size += 1;
        } 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 = 0; i == size; i++) {
            keyList.add(keys[i]);
        }
        return keyList;
    }
}

ArrayMap and Autoboxing Puzzle

Type upper bounds

在泛型中使用 extends 时,与类继承不同,此处表明的是限制与约束。

public class Box<T extends Comparable<T>> {
    //内容
}

这意味着类型 T 必须实现 Comparable 接口,即任何传递给 Box 的类型必须可以进行比较。

Lists, Sets, and ArraySet

Lists in Real Java Code

之前我们学习的是自己创建 AList 和 SLList,Java 库实际上提供了关于列表的一些内置接口和实现,我们可以使用全名进行访问:

java.util.List<Integer> L = new java.util.ArrayList<>();

也可以通过导入 Java 库的方式

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

public class Example {
    public static void main(String[] args) {
        List<Integer> L = new ArrayList<>();
        L.add(5);
        L.add(10);
        System.out.println(L);
    }
}

Sets

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

Throwing Exceptions

隐式异常会导致正常的控制流停止,我们可以使用 throw 关键词引发自己的异常,这样可以提供自己的错误消息,还可以将信息提供给程序中的错误处理代码,这样就是一个我们故意抛出的显式异常

Catching Exceptions

通过 catch 异常,我们可以防止程序崩溃,关键词 try 和 catch 会中断程序的正常流程,从而保护程序免受异常的影响。例如:

Dog d = new Dog("Lucy", "Retriever", 80);
d.becomeAngry();

try {
    d.receivePat();
} catch (Exception e) {
    System.out.println("Tried to pat: " + e);
}
System.out.println(d);

此代码的输出是

$ java ExceptionDemo
Tried to pat: java.lang.RuntimeException: grrr... snarl snarl
Lucy is a displeased Retriever weighing 80.0 standard lb units.

我们还可以使用 catch 语句来采取纠正措施

Dog d = new Dog("Lucy", "Retriever", 80);
d.becomeAngry();

try {
    d.receivePat();
} catch (Exception e) {
    System.out.println(
    "Tried to pat: " + e);
    d.eatTreat("banana");
}
d.receivePat();
System.out.println(d);

输出为:

$ java ExceptionDemo
Tried to pat: java.lang.RuntimeException: grrr... snarl snarl
Lucy munches the banana

Lucy enjoys the pat.

Lucy is a happy Retriever weighing 80.0 standard lb units.

Checked vs Unchecked Exceptions

同时,也存在一些必须处理才能通过编译器编译的异常,我们将这些称为”checked”异常,必须选中。如下图所示:

而处理它们的方式有两种:

  • catch
  • specify
// catch
public static void main(String[] args) {
    try {
        gulgate();
    } catch(IOException e) {
        System.out.println("Averted!");
    }
}

// specify
public static void main(String[] args) throws IOException {
    gulgate();
}

Iteration

Java 允许我们使用一种方便的语法(有时称为”foreach”或者”enhanced for”)来循环遍历 List,例如:

List<Integer> friends =
new ArrayList<Integer>();
friends.add(5);
friends.add(23);
friends.add(42);
for (int x : friends) {
    System.out.println(x);
}

此方法我们可以自己实现,使用迭代器,首先定义一个返回迭代器对象的方法

public Iterator<E> iterator();

然后使用它遍历列表

List<Integer> friends = new ArrayList<Integer>();
Iterator<Integer> seer = friends.iterator();

while (seer.hasNext()) {
    System.out.println(seer.next());
}

这个方法的效果与上面完全相同。

Packages

Creating a Package

  1. 将包名称放在此包中每个文件的顶部

     package jason.tinuvile.bird;
    
     public class Bird {
         private String name;
     }
    
  2. 将文件储存在具有相应文件夹名称的文件中,该文件夹的名称应该与包匹配

    <your_project_root>
    │
    └───jason
        └───tinuvile
            └───bird
                └───Bird.java
    

Default packages

任何文件顶部没有明确包名的 Java 类都将被自动视为 default 包的一部分。

JAR Files

Access Control

  • private 私有成员只有给定类中的代码才能访问,子类、包和其他外部类都无法访问;
  • package private 意味着属于同一个 package 的类可以访问,但子类不可以;
  • protected 只有同一包和子类可以访问;
  • public 访问权限对所有人打开。

Encapsulation, API, ADT

  1. 封装:如果一个模块的实现是完全隐藏的,只能通过接口访问,则称该模块是封装的。
  2. API:ADT 的 API 是构造函数和方法的列表以及每个方法的简短描述,由语法和语义规范组成。
    • 编译器验证是否符合语法,即 API 中定义的内容都存在;
    • 测试验证语义是否正确,即功能实现,语义规范常用英文写作;
  3. ADT

留下评论