What & How & Why

Exceptions, Data Structures, Recursion & GUIs

Course III notes


Exceptions and File I/O

Eexception 的定义

  • Exception 是一种 Object
  • Exception 代表了某种出现在 runtime 时期的错误事件(exceptional event)。该事件会打断正常的程序流程。
Exception 的常见例子
  • NullPointerException
  • ArrayIndexOutOfBoundsException
  • StringIndexOutOfBoundsException
  • ArithmeticException
  • ClassCastException
  • IllegalArgumentException
什么是 thrown exception

throw 代表了一个处理 exception 的过程。该过程包括:

  • 针对错误创建一个 exception object
  • 将该对象递交给 JVM 进行处理,看是否有方法可以处理该异常

一个 InputMismatch 的例子:

Exception in thread "main" java.util.InputMismatchException
        // using throwFor as an operator to hand off the ecxcption to JVM
        at java.base/java.util.Scanner.throwFor(Scanner.java:939)
        // where the exception actually made
        at java.base/java.util.Scanner.next(Scanner.java:1594)
        // tracking calls which caused the exception
        at java.base/java.util.Scanner.nextInt(Scanner.java:2258)
        at java.base/java.util.Scanner.nextInt(Scanner.java:2212)
        // where exception started
        at FahrenheitToCelsiusExceptions.main(FahrenheitToCelsiusExceptions.java:6)

The call stack

expection 的 throw 过程通常会在终端显示。这个过程被称为 call stack trace。这个过程展示了一系列的 method 导致了异常被抛出。stack 体现了这些 methods 的调用流程。

通常来说,JVM 会存储当前正在执行的 method 的状态:

  • 在其调用另一个 method 之前,JVM 会存储其 local variables 和调用 method 的入口(referenece)
  • 整个信息存储的数据结构就是一个 stack
  • 当调用 Method 时,如果需要在当前 method 中调用其他 method:
    • 首先将当前 method 的信息存储压栈(push),再进行第二个 method 的调用
    • 如果当前 method 成功完成执行,则信息出栈 (pop)
  • 可见 main() 是第一个压栈,最后一个出栈的。

The Throwable Hierarchy

Exception 类型存在以下继承关系:

  • 最顶部的是 Throwable
  • Error 代表了无法恢复的错误类型(比如内存不足)
  • Exception 代表了可以处理的错误类型

Handling Exceptions

Java 使用 trycatch 语句处理异常:

  • 需要处理(catch) 的代码段应该放与 try block 中:

try {
    statement(s)
}

  • 定义 exception 的类型:

catch (ExceptionType identifier)

  • 对该类型的 exception 的处理方法放置于 catch block 中:

catch (ExceptionType identifier) {
    handleStatement(s)
}

如果进入了 catch 阶段,程序就不会再回到 try 阶段了。

Multiple Catch Blocks

try {
 statement(s);
} catch (ExceptionType1 identifier) {  
 statement(s);
} catch (ExceptionType2 identifier) {  
 statement(s);
}

Exception Controlled Loops
  • 对 try-catch block 使用循环反复检测时,需要将执行语句置于循环之外
  • 如果是检查输入,每次检查之后都需要对 inputstream 使用 nextLine() 清理

public static void main(String[] args) {
    Scanner input = new Scanner(System.in);
    int fahrenheit = 0;
    boolean isValid = false;
    
    while(!isValid) {
        try {
            System.out.print("Enter a Fahrenheit value: ");
            fahrenheit = input.nextInt();
            isValid = true;
        } 
        catch (InputMismatchException e) {
            input.nextLine();
            System.out.println("Your input is not an int type.");
        }
    }
    double celsius = (5.0/9) * (fahrenheit - 32);
    System.out.printf("Fahrenheit: %d\n", fahrenheit);
    System.out.printf("Celsius:    %.1f\n", celsius); 
}

Exception 的优势
  • 分离错误处理逻辑
  • 可以将错误进行分类处理
Handling Multiple Exception

每一个 catching 的分支完毕之后,都不会再继续执行其他的 catch。因此,error catching 分类的顺序是非常重要的。有两点需要注意:

  • 更具体的 error catching 放在更前面:如果是泛用的异常(比如 Exception)在前,那么所有的 error 都会被该分支捕获,其他的分支永远不会被唤起。(Java 无法通过编译)
  • 根据需求对 catching 分支排序。比如类型不匹配与除 0 两种处理,如果将类型不匹配(int)置于前面,那么输入 0.0 的时候抛出 InputMismatchException 而不是 ArithmeticException
  • 可以通过调用 getMessage()getStackTrace() 获取更多异常的额外信息。

try {
    //...
}
catch (InputMismatchException ime) {
    input.nextLine();
    System.out.println("Your input is not an int type.");
}
catch (ArithmeticException ae) {
    input.nextLine();
    System.out.println(ae.getMessage());
    System.out.println(ae.getStackTrace());
}
catch (Exception e) {
    input.nextLine();
    System.out.println(e.getMessage());
}

Combined Catch Blocks

如果希望将多个异常归于一类,使用 | 运算符并列多个 exception:

catch (InputMismatchException | ArithmeticException cbe) {
    input.nextLine();
    System.out.println("Invaild Input");
}

The finally Block

finally 修饰的 block 处于 try-catch block 的最后,代表无论是否出现异常都会执行的代码段。通常可以放一些必要的清理代码:

try {
    //....
} 
catch (Exception e) {
    System.out.println(e.getMessage());
}
finally {
    input.nextLine();
}

File I/O

Input & Output 的使用

Java 中使用文件作为 Input 和 output 需要 4 个 package:

  • io.File,这个 package 提供 File class。该类型对象接收一个 String 类型的路径用于读 / 写文件。
  • io.FileNotFoundException,这个 pakeage 提供文件名不存在时的异常对象。
  • util.Scanner,提供读取对象
  • util.PrintWriter,提供写对象

ScannerPrintWriter 的初始值推荐为 null。该值可以作为关闭流的条件:在没有文件路径的情况下,这些对象的默认是 null;这种情况下不需要关闭流。

//fileScan is an Scanner object, filePrint is an PrintWriter object
if(fileScan != null) {
    fileScan.close();
}
if(filePrint != null) {
    filePrint.close();

简单的实现文件:filetest.java

FileNotFoundException

在有 File 类型的 Object 参与的程序中,FileNotFoundException 不可省略。但也无需为其指定 catch 分支;只需要声明(导入) FileNotFoundException 即可:

import java.io.FileNotFoundException;

exception 分为 checked 和 unchecked 两种类型。checked exception 指必须做特别处理的 exception,比如上面的例子。unchecked exception 通常指某些多变的,非常难以管理的 excpetion。这种 expetion 不会留具体的处理方式。

input
  1. 以具体路径为参数实例化 File 对象
  2. 使用 Scanner 对象读取该 File 对象的内容
  3. 使用 Scanner 的 method 对文件进行读取(比如 nextLine

String inputFileName = "text.txt"
File fileIn = new File(inputFileName);
Scanner fileScan = new Scanner(fileIn);

output
  1. 同样需要使用路径实例化的 File 对象
  2. 使用 PrintWriter 对象读取该 File 对象的内容
  3. 使用 各种 print 将内容写入到文件中

File fileOut = new File(keyword + "In" + inputFileName);
filePrint = new PrintWriter(fileOut);
filePrint.printf("Line %d contains %s\n",lineNum, keyword);

重要的 method
  • String.contians(keyword):检查 String 中是包含 keyword 关键字

自定义 exception

我们可以对 exception 进行更加的特例化:只需要根据需求继承对应的 exception 类即可,比如:

public class DivideByZeroException extends ArithmeticException{
    public DivideByZeroException() {
        super("Divide by zero.");
    }
}
这里的 super(“Divide by zero.”) 调用了父类的构造函数,String 参数会作为 getMessage() 的返回值。

使用 throw operator

与 try-catch 不同,throw 允许我们手动引发异常:

fahrenheit = input.nextInt();
if (fahrenheit == 0) {
    throw new DivideByZeroException();
}
引发异常后,throw 会先在同 method 的内容中寻找对应的的 error handing (catch)。如果需要我们实现,写法与 try-catch 一致:
catch (DivideByZeroException de) {
    System.out.println("invalid number.");
}

throw 通常用于处理一些我们明确知道的异常,比如上述的除数为 0 的情况。try-catch 更像是一种防患于未然的机制,用于保证程序不会因潜在的问题导致崩溃。

Delimited Files

什么是 Delimited Files

Delimited Files,又称 CSV,是一种可以保证格式persistent)的,用于存储结构性数据的文本文件。比如下面的数组:

public static void main(String[] args) {
    Wolf[] pack = {
        new Wolf(17.1, 2),
        new Wolf(3, 10),
        new Wolf(9.2, 7),
        new Wolf(9.1, 8),
    };
如果直接打印到终端,那么结果是:
[Rank 2, Size 17.1, Rank 10, Size 3.0, Rank 7, Size 9.2, Rank 8, Size 9.1]
可以看到的是,终端打印有太多的局限:

  • 无法保证格式
  • 无法保留结果
  • 较大的数据可能无法一次性全输出

CSV 文件可以解决该问题。 CSV 文件是一种文本文件,其内部不同的数据使用逗号隔开。比如上面例子中的数据,写成 CSV 的格式:

2,17.1
10,3.0
7,9.2
8,9.1

输出 CSV

输出 CSV 只要以上述的格式,使用 PrintWriter 对象将其输出到文本中即可:

File fileOut = new File("SortedWolves.csv");
PrintWriter filePrint = new PrintWriter(fileOut);
filePrint.println(wolf.getRank() + "," + wolf.getSize());

处理 CSV

JAVA 中提供了 String.split(char) 方法用于按 char 分割字符串。返回的结果是一个包含了各个分割部分的 String[]。因此,如果将分割符设置为逗号,那么就可以从 CSV 文件中提取数据:

double[] allWeights = new double[10];
fileScan = new Scanner(fileIn);
String line = null;
while (fileScan.hasNextLine()) {
    line = fileScan.nextLine();
    tokens = line.split(",");
    allWeights[index] = Double.parseDouble(tokens[1]);
    index++;
}
需要注意,得到的是 String[],某些场景需要转换为指定格式的数据。

Regular Expressions and split()

split() 支持正则表达式Regular Expressions)。下面是一个使用大写字母作为间隔的分割实例:

public static void main(String []args) {
    String chant = "Java Is The Best!";
    String[] tokens = chant.split("[A-Z]+");
    for (String x : tokens){
        System.out.println(x);
    }
}

使用 Scanner 处理 CSV

Scanner 可以像处理 stream 一样处理 String(也就是其成员 method 都可以用于处理 String)。我们可以使用 useDelimiter() method 来指定间隔符,然后使用 next 系列 method 来读取每一个部分:

fileScan = new Scanner(fileIn);
String line = null;
while (fileScan.hasNextLine()) {
    line = fileScan.nextLine();
    wolfScan = new Scanner(line);
    wolfScan.useDelimiter(",");
    wolfScan.nextInt(); //consume unused rank token
    allWeights[index] = wolfScan.nextDouble();
    index++;
}

Lists

List Java 中以 Interface 的形式出现。不同类型的数据都会基于该 Interface 设计 class。LIst 的几个基本方法:

  • add(E e):添加元素
  • clear() :清空 List
  • contains(Object o) :查询是否包含某个元素
  • remove(index) : 删除指定位置的元素
  • size():返回 List 大小

ArrayList

  • an implementation of the List interface with an array as the underlying data structure for storing items
  • Arraylist 声明了一个 名为 elementData 的 array instance; 类型为 Object[] elementData,以及一系列的配套 methods。
  • 如果 array 空间不够,会创建一个新的 Object array
Creating an ArrayList
  • 创建时不需要指定 type(Raw type)
  • 当 parameter 提供的时候,arraylist 会转换为对应的版本类型(parameterized type
  • 未指定长度的 arraylist 长度为 10(10 个值为 null 的连续地址)。arraylist 用 Capacity 来描述长度。
  • 如果不指定类型,被存储的内容类型可以不同

//generic creation, 10 capacity
ArrayList playlist = new ArrayList();
//generic creation, 5 capacity
ArrayList playlist = new ArrayList(5);
//type sepcified creataion
ArrayList<elementType> aList = new ArrayList<elementType>(initialCapacity);
//type sepcified creataion (after java 7). type on righthandside can be omited.
ArrayList<String> playlist = new ArrayList<>(5);

指定类型的声明方法只限制了可以调用的 mothod;也就是说,其声明类型依然是 Object[],而实例(对象)类型才是被指定的类型。

  • 其他实现的 interface: Serializable, Cloneable, Iterable<E>, Collection<E>, List<E>, RandomAccess

ArrayList 的相关操作

ArrayList<String> playList = new ArrayList<>();
//adding element to the end of the list(push_back())
playList.add("Humpty Dumpty");
//remove
playList.remove("Humpty Dumpty");
//print, will call toString()
System.out.println(playList.toString());

Autoboxing

Arraylist 定义的类型是 Object[]。如果需要在 arraylist 中使用 primtive type,那么需要使用 Java 中对应的 Object(match wrapper type)。比如 int 对应 Integer Object。这个把 primitive type 自动转化为 对应的 wrapper type 的过程被称为 AutoBoxing

public static void main(String[] args) {
    ArrayList<Integer> intList = new ArrayList<>();
    intList.add(1);
    intList.add(2);
    intList.add(3);
    for(int num : intList) {
        System.out.println(num);
    }
}

LinkedLists

Java 中自带 GenericLinkedList 类。初始化方法:

GenericLinkedList<type> newList = new GenericLinkedList<>(iniParamterList);

Generic Classes

在 Java 中,如果声明类型与对象类型不一致,在调用对象类型中的 method 时,需要进行 casting 确保调用的对象和方法的一致性:

//class Bin info for demo
private Object content;
public Bin(Object content) {
    this.content = content;
}
public Object getContent() {
    return content;
}
//instantiate
Bin car = new Car("yuhina", "spark", 2037));
//call
((Car).car).getContent();
实际上在 Java 中,我们可以通过对 Bin 类进行类型特化来替代每次调用都要 casting 的步骤。写法如下:
//simiar to template in C++
//T can be replaced with any meanful name
Class className<T>
之后所有与 generic 相关的 name, 返回值就都可以用 T 修饰了:
public class Bin<T> {
    private Object content;
    public Bin(T content) {
        this.content = content;
    }
    public T getContent() {
        return content;
    }
}
//instantiate, a specific type needed
Bin<Car> car = new Car("yuhina", "spark", 2037));
//call, casting is nolonger needed.
car.getContent();

包含多种类型的 generic class

如果希望一个 generic class 特化多个类型,可以直接在 typename list 里面添加:

className<T1,T2,...Tn>
在通用类的内部可以使用声明类型使指定的变量和方法一致:
public class Bin<X, Y> {
    private X content1;
    private Y content2;
    
    public X getContent1() {
        return content1;
    }
    
    public void setContent(Y content2) {
    this.content2 = content2;
}
//intansiate
Bin<Car, Dog> = new Bin(Car("Yuhina", "Spark", 2037), Dog("puppy"));

其他 type parameter 的用法
  • 限制 T 的类型必须是某个类的子类:

//T must be the subclass type of the Insert class
Bin<T extends Insert>

  • 限制 T 的类型必须是某个 interface 的实现:

Bin<T extends Comparable>

  • 以上限制可以叠加:

Bin<T extends Insert & Comparable & Groomable>

实例:结合使用 type parameter 和 Comparable

//T is an Comparable type(can call compareTo())
public class Compartment<T extends Comparable> {
    private T content1;
    private T content2;

    public Compartment(T content1, T content2) {
        this.content1 = content1;
        this.content2 = content2;
    }

    public boolean Greater() {
        return content1.compareTo(content2) >= 0 ? true : false;
    }

    public static void main(String[] args) {
        Compartment<String> test = new Compartment<>("Hello", "Hello");
        System.out.println(test.Greater());
    }
}

LinkedList 的原理

ArrayList 的几个劣势:

  • 初始化需要占指定空间
  • 插入非尾部元素需要移动其他元素
LinkedList 的基础元素
  • Node:
    • 存储数据
    • 存储指向下一个元素的地址

private class Node<E>
{
    E data;
    //Node<E> type is an reference(address) 
    Node<E> next;

    Node(E data, Node<E> next) {
        this.data = data;
        this.next = next;
    }
}
初始化:
//empty list
Node<E> mylist = new Node<E>(newData, null);

head 与 current

链表需要同时管理两个指针:

  • head 指向链表的起始元素地址
  • current 指向当前元素所在的地址

因此,head 的初始化为首元素的地址。如果值为 null,则链表不存在元素。因此,链表的初始化可以写成下面的形式:

//head has default value null
//right hand side equals new Node<E>(newData, null);
//after init, head points to the first Node
head = new Node<E>(newData, head);

addToFront()

该方式会在现有的 head 之前添加元素:

  1. 创建新节点:将该节点指向 head
  2. 更新 head:让 head 指向刚被创建的新节点



很容易看出来, addToFrount() 方法无论是初始化还是添加节点到现有链表,其逻辑都如下:

head = new Node<E>(newData, head);

front traversal

遍历需要使用到 current 指针:

  1. 遍历从 head 开始:current 的初始值与 head 相同
  2. 循环中,currrent 的值会更新为 next 中的地址,也就是下一个节点的地址
  3. current 值为 null 时,表示当前链表已经没有元素,遍历结束

Node current = head;
while(current != null)
{
    //do sth
    current = current.next;
}

addToRear()

该方式中,新建的节点永远都处于链表的最后一位。

  1. 初始化节点:节点的 next 初始值为 null,因为每次添加的元素都会成为链表最后一个元素
  2. 如果链表只有一个节点,那么当前的节点就是 head 的位置
  3. 否则,使用 current 遍历链表,直到遍历到最后一位节点,
  4. 修改之前的节点指向地址为之前初始化的节点。

注意循环的条件:current.next != null

//new date will always be the end of the list
Node<E> node = new Node<E> (newData, null);
//init current
Node<E> current = head;

//if there is only one node in the list, then point head to the node
if (head == null) {
    head = node;
}
//else, trverse to the end of the list
else {
    while(current.next != null) {
    current = current.next;
    }
    //point the last node to the new created node to make it as the newest last one.
    current.next = node;
}

注意前置循环添加元素和后置循环添加元素的两个循环条件不一样:

  • 前置需要验证的是当前节点是否为空
  • 后置需要验证的是当前节点指向的节点是否为空,即是否为最后一位元素
toString()

链表中的打印需要遍历整个链表。默认从开头开始打印。

public String toString()
{
    String result = "";

    Node<E> current = head;
    while (current != null) {
        result = result + current.data.toString() + "\n";
        current = current.next;
    }
    return result;
}

链表中的遍历需要维护 current 指针。该循环的三个必要条件:

  • current 从 head 处开始
  • current 停止的条件是 current 值不会 null
  • current 的迭代方式是让其指向下一个节点的地址
contains()
  • 如果链表为空,直接返回 false
  • 如果不为空,则遍历查询比较(使用 equal())是否存在匹配关键词的元素。有则返回 true
  • 搜索完毕没找到返回 false

public boolean contains(E key)
{
    if(isEmpty()) {
        return false;
    }
    boolean isFound = false;
    Node<E> current = head;
    while(current != null) {
        if (key.equals(current.data)) {
            isFound = true;
            break;
        }
    }
    return isFound;
}

removeFromFront()

删除节点前都需要考虑链表为空的情况。

由于 Java 的 GC 机制,删除的节点本身资源的释放会自动进行。我们只需要重新将 head 指向第二个节点即可。具体步骤:

  • 检测当前链表是否为空,如为空返回 null
  • 获取被删除节点中的数据
  • 重新定向 head
  • 返回被删除节点的数据

public E removeFromFront()
{
    if(isEmpty()) { 
        return null;
    }
    else {
        E removedData = head.data;
        head = head.next;
        return removedData;
    }
}

removeFromRear()

由于需要删除最后一个节点,removeFromRear() 需要对倒数第二个节点进行操作。又因为访问链表尾部元素必须通过遍历进行,如果以倒数第二个节点为循环结束点,循环条件需要从:

while(current.next != null)
更改为:
while(current.next.next != null)
因为上述这个条件是默认链表中至少存在 2 个节点的;因此必须对链表中只存在一个节点的情况进行单独处理。因此,整个 removeFromRear 的逻辑为:

  1. 如果链表为空,返回 full
  2. 如果链表长度为 1,也就是 head.next = null 时:
    1. 缓存节点的数据
    2. 将 head 设为 null (代表空链表)
    3. 返回缓存数据
  3. 如果链表长度大于 1:
    1. 遍历到倒数第二个节点
    2. 缓存最后一个节点的数据
    3. 将倒数第二个节点的指向设为 null,使之成为最后一个节点

public E removeFromRear()
{
    E removedData;
    if(isEmpty()) {
        return null;
    }
    
    else if (head.next == null) {
        removedData = head.data;
        head = null;
    }
    else {
        Node<E> current = head;
        while(current.next.next != null)
        {
            current = current.next;
        }
        removedData = current.next.data;
        current.next = null;
    }
    return removedData;
}

Linked Lists vs. ArrayLists

ArrayList 有数据访问上的优势:

  • random access 非常快(通过下标即可)
  • 链表需要通过遍历才能进行访问,最差情况为 $O(n)$

Recursion

  • method 可以调用其本身
  • mehod 应该具有某种机制作为终止条件,允许返回

递归本身可以想象成堆栈的过程。递归调用一次就等于 push 一次,栈顶的是递归的 base case。

递归正确运行的三个条件
  • 终止条件(base call):不需要进行递归调用就可以进行 return 的语句
  • 递减步骤(reduction step):使递归调用朝着终止条件的方向进行
  • 递归调用(recursive call)
可能用于改进的手段
  • 对输入范围进行额外的限定
  • 使用 IllegalArgumentException 控制输入范围(如果不满足条件,则抛出该异常)

JavaFX

  • Stage: window
  • Scene: 内容

JavaFX essentials

入口函数的定义

使用 JavaFX 的类会继承自 javafx.application.Application。该类中定义了一个抽象入口函数 start()。需要使用 javaFX 的类都需要实现该函数(可以认为是 JavaFX 里的 main()

public class JavaFXTest extends Application {
    public void start(Stage mainStage) {
    //....
    }
}

结构总览
  • Stage:当前的窗口
  • Scene:由窗口中的元素以及其布局组成
Events Handling

Events 代表了用户的操作。JavaFX 中的 Event 继承自是 ActionEvent



如果需要处理 Events,需要实例化对应类型的 Events,比如鼠标点击按钮:

btn.setOnAction(new CustomEventHandler());
Events 的初始化需要 EventHandler 类型的实例来处理用户执行完操作以后到底会发生什么。为了将 EventsHandler 联系起来,JavaFX规定:

  • Handler 中必须定义一个 handle() 函数
    • 通过继承接口 EventHandler 实现。EventHandler 包含了抽象函数 handle()
    • 用户需要根据 Event 的类型决定 EventHandler 的特化类型(点击鼠标的类型是 ActionEvent

比如为点击按钮设置一个名为 CustomEventHandler 的类:

//CustomEventHandler implements EventHandler
//click button action has type ActionEvent
private class CustomEventHandler implements EventHandler<ActionEvent> {
     
    //rewrite handle
    //handle parameter has type T to ensure the parameter macting the event type
    public void handle(ActionEvent event) {
        System.out.println("Hello Wolrd!");
    }
}
CustomEventHandler 定义完毕后,我们将其传递给了 setOnActrion() 成员方法。该步骤相当于我们将其注册为 Event Listener,用于反馈结果给点击按钮的行为。

  • EventHandler 必须是 private 类型,并且是内部类(Inner class
Panes & Scene Graphs

Panes 为整个 Stage 提供布局。其扮演了一个组件容器的角色:

StackPane root = new StackPane();

之后创建的组件会叠加到之前创建的组件上面

实例化 Pane 之后,我们就可以将之间创建的组件通过 getChildren() 成员放入该容器中了:

//adding button element to the container
root.getChildren().add(btn);
Pane 与组件之间的关系是包含(contains)关系,可以用 Scene Graphs 的方式来描述(图中的粗线代表包含):


其他可选的 layout 和组件有:



  • Layout 类别中的类都是 StackPane 的子类,代表了不同的布局方式。如果没有合适的,可以自己通过继承 Pane 来自定义布局。参考:文档
  • 元素组件被称为 leaf node
  • Layout 本身不必是根部节点,也可以是分支节点(嵌套布局)

Anonymous Inner Classes

EventHandler 可以用 inner class 的方式实现:

EventHandler<ActionEvent> CustomEventHandler = new EventHandler<>() {
    public void handle(ActionEvent event) {
        System.out.println("Helloworld");
    };
};
//notice CustomEventHandler is not a cstr. it is an reference name now.
btn.setOnAction(CustomEventHandler);
<code>
如果只使用一次,可以写成匿名的形式直接作为 ''setOnAction()'' 的参数:
<code js>
btn.setOnAction(
    new EventHandler<ActionEvent>() {
        public void handle(ActionEvent event) {
            System.out.println("hello world");
        };
    }
);

Lambda Expressions

上述的 EventHandler 还可以使用 Lambda 表达式进一步的改进:

btn.setOnAction(event -> System.out.println("helloWorld"));

  • handle() 可以省略,因为 EventHandler 中只有一个函数
  • ActionEvent 也可以省略,Java 可以根据 EventHandler 自动推断类型
  • 如果 handle() 的定义之后一行,连大括号都可以省去

Addtional Operations

ComboBox
  • 定义 ComboBox

ComboBox<String> pickScaleFrom = new ComboBox<>();

  • 添加选项到 Combobox

pickScaleFrom.getItems().addAll("Fahrenheit", "Celsius");

  • 设定默认选项

pickScaleFrom.getSelectionModel().selectFirst();
pickScaleTo.getSelectionModel().selectLast();

  • 从 ComboBox 中获取选项值

String scaleFrom = pickScaleFrom.getValue();

  • 打印值到 Label 区域

result.setText(String.format("%.2f", conversionResult));

Label
  • 创建 Label

abel from = new Label("From:");

TextField
  • 创建 TextField

TextField userInput = new TextField();

  • 获取 TextField 中的内容(in string format)

//userInput is an TextField instance
userInput.getCharacters().toString();

Error dialog box

//define
Alert a = new Alert(AlertType.ERROR);
//title
a.setTitle("Error");
//error message
a.setHeaderText("Invaild Temperature");
//details
a.setContentText("That is not a valid tempature");
//keeping the dialog box show up, pause the program
a.showAndWait();

Layout
  • 创建 Layout

//keep the element in the same row
HBox input = new HBox();
//
VBox scales = new VBox();

  • 设置对齐方式:

input.setAlignment(Pos.CENTER);

  • 设置间距

scales.setSpacing(10);

  • Layout 的嵌套

root.getChildren().addAll(input, scales, convertButton, result);