返回随笔Java全栈
完成ENGINEERING NOTE

Java

系统整理 Java 程序结构、输入输出、数组、流程控制、对象与基础语言特性。

Java

一、基础部分

程序设计结构

  • main方法需要为public static void
  • 文件名需要和公共类的名字相同,且使用.java作为扩展名

注释

  • 可以通过/** **/来生成注释文档

swith语句

  • 可以将switch语句作为右值
  • 使用分号而不是break
  • case使用->
public class Main {
    public static void main(String[] args) {
        int a = 0;
        int b = switch (a) {
                case 1 -> 10;
                case 2 -> 20;
                default -> 30;
        }
    }
}

输入和输出

  • 输出 java使用System.out的方法print系列进行输出:
public class Main {
    public static void main(String[] args) {
        System.out.print("Hello World");    //print打印一个对象
        System.out.println("Hello World");  //println打印一个对象并换行
    }
}
  • 输入 要想使用输入,必须包含包util
import java.util.*;

使用输入前需要创建一个Scanner实例,将该实例与输入流绑定,再使用该输入流进行读取操作:

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner cin = new Scanner(System.in);
        int a = cin.next();
        String b = cin.nextLine();
    }
}

以下是输入Scanner实例的方法:

String cin.next()           //读取下一个单词的输入(空格为止
String cin.nextLine()       //读取下一行的输入(换行符为止
int cin.nextInt()           //读取下一个int
double cin.nextDouble()     //读取下一个double
boolean cin.hasNext()       //检测输入中是否还有单词
boolean cin.hasNextInt()
boolean cin.hasNextDouble()

数组

Java中的数组和C中的指针数组类似。

int[] array = new int[10];    //初始化一个长度为10的数组,10可以替换为变量
int[] array = { 1, 2, 3, 4 }; //以列表初始化数组
new int[] { 1, 2, 3, 4 };     //声明一个无名数组
array = new int[] { 1, 2, 3, 4 };  //使用无名数组赋值
  • 数组拷贝 若使用等号拷贝数组,原数组和拷贝数组将指向同一块内存空间,意味着对其中一个的修改也会影响另一个:
int[] a = { 1, 2, 3 };
int[] b = a;
b[0] = 2;    //a[0]也被修改了

可以使用Arrays.copyOf()来拷贝到新的数组中:

int[] a = { 1, 2, 3, 4 };
int[] b = Arrays.copyOf(a, 4);   //第一个参数为要拷贝的数组,第二个是拷贝长度。

//可以通过这种方法增加数组长度
b = Arrays.copyOf(b, 2 * b.lenth);
  • 数组排序 可以使用Arrays.sort()对数组排序:
int[] a = { 2, 34, 12, 3, 1 };
Arrays.sort(a);

var字段

相当于auto,若可以从右边的表达式中推出变量类型,则可以使用var自动推断变量类型。多用于临时变量。

null字段

可以将对象声明为null值,但如果对其使用方法,将会导致NullPointerException异常:

Player a = null;
String str = a.getName();   //抛出异常

二、面向对象部分

构造器

构造器是构造函数的另一种叫法。 构造器无法使用C++风格的列表初始化。 可以在类中加入代码块,在构造对象时这些代码块会调用:

public class Player {
    public int ID;

    {
        this.ID = Math.random();
    }

    public Player(int ID)
    {
        this.ID = ID;
    }
}

Java中没有析构器,它能够自动完成垃圾回收。

方法

Java中的实现不能放置于类外。放置于类内并不意味着声明为内联。若遇到同名问题,Java会发生错误,而不是同名遮掩机制。

  • main方法 每一个类都可以有一个main方法,可以在其中包含示例代码,单独运行该类文件时就可以运行该代码。

继承

Java中没有private和protected继承,仅有public继承,其语法如下:

public class A extends B {

}
  • 在子类中使用超类方法 在子类中调用超类方法需要用到关键字super:
public class A extends B {
    private String name;

    public A(String str1, String str2)
    {
        super(str2);                    //super调用基类构造器
        this.name = str1;
    }

    public String get()
    {
        String baseStr = super.get();   //super调用基类方法
        return name + baseStr;
    }
}
  • 多态 Java中默认方法为virtual的,若不需要为虚,则使用关键字final。
  • instanceof运算符 用于检查对象是否为某类的实例。

Object超类

在java中,所有类型都继承自Object超类,这是一个功能很强大的类,所有子类都可以使用其方法(如equals。

  • equals方法 equals方法有两个版本,一是成员函数版本,二是静态方法版本。
public class Main {
    public static void main {
        Player pl = new Player("KringKoter");
        Player p2 = new Player("Afternoon Tea");

        System.out.print(p1.equals(p2));             //成员函数版本
        System.out.print(Objects.equals(p1, p2));    //静态方法版本(注意是Objects

    }
}
  • toString方法 此方法将类转换为字符串。当遇到对象+字符串时,将自动调用该对象的toString方法。 每个类都需要覆写一次toString方法,如果不进行覆写, 调用时将生成类名+散列码的字符串。
  • hashcode方法 为对象生成一个散列码。
  • getClass方法 返回一个类对象,保存了有关对象的信息。

ArrayList

类似于vector容器。ArrayList可以使用菱形语法:

public class Main {
    public static void main(String[] args) [
        ArrayList<int> array1 = new ArrayList<>();      //菱形语法
        var array2 = new ArrayList<int>();              //使用var自动类型推断
        ArrayList<int> array3 = new ArrayList<>(100);   //指定初始长度
    ]
}

包

  • 导入包 使用import来导入包和包中的所有类,其中,.*只能导入一个包,不能同时导入例如java为前缀的所有包:
import java.lang.System;         //导入System包
import java.util.*;              //导入util包
import java.*.*;                 //不能这样写

在两个包中有同名成员时,可以显示指出使用哪一个包的成员:

import java.util.*;
import java.sql.*;        //两个包中都有Date类

import java.util.Date;    //显式指出使用util包的Date类

如果两个包的同名成员都需要使用,则在使用中加上相应的前缀:

var startTime = new java.util.Date();
var today = new java.sql.Date();
  • 在包中添加类 若需要在包中添加类,需要将包名放在类的开头。包名使用一个因特网域名的倒序(如Player.com变为com.Player)。如果未在包中放置package语句,则包为无名包。 以下是Player.java的包声明语法,其文件放置于com\Player\corejava中:
package com.Player.corejava

对象包装

有的时候需要将int这样的基本类型变为对象类型,所有基本类型都有一个与之对应的类。包装器类不可变,不能修改其中的数据,且不能派生其子类。诸如ArrayList的容器类无法使用基本类型,此时可以使用包装器类将基本类型包装:

ArrayList<int> array = new ArrayList<>();      //非法
ArrayList<Integer> array = new ArrayList<>();  //合法

可以直接传递基本类型给array,此时基本类型将自动包装为对象类型;反之,对象类型可以直接赋值给基本类型,称之自动拆箱。

array.add(1);          //自动装箱
int i = array.get(2);  //自动拆箱

由于java中==运算符比较的是两者是否指向同一片内存空间,对于两个相同包装器对象的比较很有可能失败。此时需要使用equals方法:

Integer int1 = 1000;
Integer int2 = 1000;
if (int1 == int2) return 1;      //不能这样比较
if (int1.equals(int2)) return 2; //应该这样比较

可变参数个数

使用…语法可以提供多个且数量不定的参数:

public class Main {
    public static int func(int... value) {
        int result = 0;
        for (var i : value)
            result += i;
        return result;
    }
}

抽象类和抽象方法

使用关键字abstract。包含一个或多个抽象方法的类需要声明为抽象。

public abstract class Super {
    public abstract void get();
}

Lambda表达式

基本语法:

(String fisrt, String second) -> first.length() - second.length()
(参数列表)                     -> 表达式体

如果没有大括号,无需指定返回类型。Java会自动推断返回类型。如果使用了大括号,则需要手动返回值,且不能使用单一情况的表达式:

(String first, String second) -> {
    if (first.length() - first.length() > 0)
        return 1;
    else
        return 2;
}

三、接口

声明接口

接口是对希望符合接口的类的一组需求,如果需要使用某方法,意味着类必须实现某接口。如果要使用Arrays类中的sort函数,则需要实现Comparable接口。

public interface Compareable<T> {
    int compareTo(T t);
}

接口里的方法不提供实现,且自动声明为public。要声明一个类实现某接口,需要将类声明为实现给定的接口,且对接口中的方法提供定义。要声明一个类实现某接口,需要使用关键字implement:

public class Player implements Complarable<Player> {
    private int level;

    public int compareTo(Player player) {
        return Intager.compare(this.level, player.level);
    }
}

接口的属性

接口不是类,不能使用new来声明一个接口,但是可以将一个接口变量与实现了这个接口的一个具体对象绑定。 接口也有继承体系,可以使用extends关键字拓展接口,使其从泛用性强拓展到专用性强:

public interface A {
    void func1();
}

public interface B extends A {
    void func2();
}

public interface C extends B {
    int level;
    void func3();
}

接口中不能包含实例字段,但可以包含常量。在接口内的实例字段自动声明为public static final。 可以将接口方法显式标记为public,将实例字段标记为public static final,但java语言规范建议不要使用冗余的关键字。

多个接口

Java没有MI,但是可以实现多个接口。在接口中使用逗号隔开:

public class Player implements A, B {
    ...
}

默认方法

可以在接口中使用default来提供默认方法,这样实现接口的类可以不提供其实现:

public interface A<T> {
    default int compare(T t) {
        return 0;
    }
}

默认方法冲突

如果同时实现了两个接口,且这两个接口内有签名一致的默认方法,需要手动指定使用哪一个方法:

interface Compare1<T> {
    default int compare(T t) {
        return 0;
    }
}

interface Compare2<T> {
     default int compare(T t) {
         return 1;
     }
}

public class Player implements Compare2<Player>, Compare1<Player> {
    public int compare(Player player) {
        return Compare1.super.compare(player);  //手动指定使用Compare1的compare方法
    }
}

四、泛型和集合

声明泛型类

Java中无需使用template来指定泛型:

public class name<T> {
    private T element;

    public name() { element = null; }
    public name(T element) {
        this.element = element;
    }

    public T getElement() {
        return this.element;
    }

    public void setElement(T elemnet) {
        this.element = element;
    }
}

声明泛型方法

泛型方法可以在普通类中声明,也可以在泛型类中声明。

public class name {
    public <T> T function() {
        return T;
    }
}

集合和Collection接口

Java中的集合类似于STL,包括了ArrayList,SortedMap,set等容器。这些容器都实现自Collection接口(SortedMap实现自Map接口)。要想声明自己的数集,并使用相应的算法,需要实现Collection接口:

public class MySet<E> implements Collection<E> {
    public int size() {
        reutrn size;
    }

    public boolean isEmpty {
        return size == 0;
    }

    ...
}

如果不是确实需要实现自己的集合,Java库中的现有集合就能满足需求。Collection接口中的具体方法见《Java核心技术·卷一》383页。

迭代器

Java中的迭代器与C++中的不同,它并不是类似于数组索引的存在,而是相间于两个元素之间。

public class Main {
    public static void main(String[] args) {
        ArrayList<Integer> array = new ArrayList<>();
        for (int i = 0; i < 10; i++) {               //向集合中添加元素
            array.add(i);
        }

        Interator<Integer> iter = array.interator(); //获得array的迭代器,该迭代器从首前开始。
        for (iter.hasNext())                         //hasNext函数检查是否有下一个元素
        {
            int a = iter.next();                     //将迭代器移向下一个元素之后,并返回刚才的元素。
            System.out.println(a);
        }

        itera.remove()                               //删除上次调用next的元素。

    }
}

有关迭代器的方法见《Java核心技术·卷一》394页。

散列集(HashSet)

有一种众所周知的数据结构,可以用来快速查找对象,被称作散列表。散列表为每个对象计算一个散列码。要想查找每个元素在表中的位置,要先计算其散列码,再通过运算找到桶的编号。如果桶中已经有了元素,这种现象被称作散列冲突(Hash Collision),这时需要将新对象与原对象进行比较,查看这个数据是否存在。 Java库提供了一个散列集,可以用add方法插入元素,使用contains方法查看元素是否存在。当访问元素时,会通过一种看起来随机的方式,只有不关心集合中元素顺序时才使用HashSet。 这个实例程序读取一行字符,并打印其前20个不重复的单词。

public class Main {
    public static void main(String[] args) {
        Set<String> stringSet = new HashSet<>();

        var cin = new Scanner(System.in);
        while (cin.hasNext()) {
            String temp = cin.nextLine();
            stringSet.add(temp);
        }

        var iter = stringSet.iterator();
        while (iter.hasNext()) {
            String temp = iter.next();
            System.out.println(temp);
        }
    }
}

有序树集(TreeSet)

相较于HashSet,基于红黑树排序的树集在add数据时自动排序。添加数据时树集较慢,但查找较快。要使用树集,必须能够比较元素。这些元素必须实现了Comparable接口。 下面的实例程序使用实现了Comparable接口的测试类Item,比较其中的number实例字段。

public class Main {
    public static void main(String[] args) {
        Set<Item> itemSet = new TreeSet<>();

        for (int i = 0; i < 20; i++) {
            Random random = new Random();
            itemSet.add(new Item(random.nextInt(101)));
        }

        Iterator<Item> iter = itemSet.iterator();
        while (iter.hasNext()) {
            System.out.println(iter.next());
        }
    }
}

public class Item implements Comparable<Item>{
    private int number;

    public Item(int number) {
        this.number = number;
    }

    public int compareTo(Item item) {
        return number - item.number;
    }

    @Override
    public String toString() {
        return "Item{" +
                "number=" + number +
                '}';
    }
}

映射(Map)

映射分为散列映射和树映射,区别在于散列为无序而树为有序,它们都实现了Map接口。使用put函数向映射中添加元素,使用get来获取元素。在使用get时,必须提供key,因此必须记住key。 如果没有找到元素,则返回null,但null并不能包含有效信息,可以使用getOrDefault来使用自定义的默认值:

Map<String, String> map = new HashMap<>();
int a = map.getOrDefault("wobuzhidao", 100);    //a将为默认值100

可以使用forEach方法,该方法接受一个lambda表达式,映射中的元素会依次调用此表达式。

public class Main {
    public static void main(String[] args) {
        Map<Integer, String> stringMap = new HashMap<>();
        stringMap.put(20040528, "JSKringKoter");
        stringMap.put(31061231, "FlyingDream");
        stringMap.put(31060825, "Ou'yang Xingyu");

        stringMap.put(20040930, "Afternoontea");
        stringMap.put(20040528, "JSKring");

        stringMap.remove(2004);

        stringMap.forEach((k, v) ->
                System.out.println("key=" + k + " value=" + v));
    }
}

有关映射的方法在《Java核心技术·卷一》408页。

更新映射条目问题

如果需要更换一个条目,但不知道是否存在,需要进行检查:

Map<String, Integer> counts = new HashMap<>();
counts.put("word", counts.get("word") + 1);

这些代码在word出现后将其引用计数+1,但如果word第一次出现,则会抛出NullPointerException异常。补救方法是,使用getOrDefault函数:

counts.put("word", counts.getOrDefault("word", 0) + 1);

链接散列集与映射

LinkedHashSet和LinkedHashMap会记住元素插入的顺序,使其看起来以一种有序的方法排序。

排序与散排

Collections类实现了一组方法,可以将给定的集合排列或散排,前提是其实现了Comparable接口。

  • 排序:Collections.sort()
  • 散排:Collections.shuffle()
public class Main {
    public static void main(String[] args) {
        ArrayList<Integer> integerArrayList = new ArrayList<>();
        for (int i = 0; i < 30; i++) {
            integerArrayList.add(i);
        }

        Collections.shuffle(integerArrayList);
        System.out.println(integerArrayList);
        Collections.sort(integerArrayList);
        System.out.println(integerArrayList);
    }
}

List接口也有一个sort函数,但其接受一个比较器对象。Comparator.reverseOrder函数返回一个将元素倒序排序的比较器。下面的代码将integerArrayLIst的元素按逆序排序。

integerArrayList.sort(Comparator.reverseOrder());

交集

使用retainAll函数可以快速生产交集,这个函数接受一个Collections,保留两者都有的元素,删除没有的元素。

ArrayList<Integer> oldList = new ArrayList<>();
ArrayList<Integer> newList = new ArrayList<>();

oldList.retainAll(newList);

五、图形用户界面程序设计

创建窗体

Swing类位于javax中,其被认为是一个拓展包。在每个Swing程序中,Swing组件必须由事件分派线程配置,它将鼠标点击和按键等事件传递给用户界面组件,下面的代码用来在事件分派线程中执行语句:

EventQueue.invokeLater(() ->
{
    statements
});

想要显示一个界面,需要先声明一个继承了JFrame的类。这个类包含了窗体的基本信息,如长度和高度。在类的构造器中,可以通过函数来设置构造时的基本属性:

class SimpleFrame extends JFrame {
    static final int DEFAULT_WIDTH = 300;
    static final int DEFAULT_HIGHT = 400;

    public SimpleFrame() {
        setSize(DEFAULT_WIDTH, DEFAULT_HIGHT);
    }
}

然后在主函数中创建这个类的对象,窗体对象创建后可以对其进行配置:

public static void main(String[] args) {
    EventQueue.invokeLater(() -> {
        var frame = new SimpleFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);  //设置退出窗体时程序即退出
        frame.setLocation(100, 100);                           //设置窗体位置
        frame.setResizble(true);                               //设置是否可以调整大小
        frame.setVisible(true);                                //设置为可见
    });
}
  • 属性 如下的一组代码称作窗体的一个属性,属性由一个get和一个set组成(对于boolean类型,get为is),将get或set后的单词小写即为其属性:
public String getTitle()
public void setTitle(String title)

以及:

public boolean isResizable();
public void setResizble(boolean resizable)
  • 调整合适的窗体大小 要确定合适的窗体大小,需要先知道屏幕的大小。调用Toolkit类的静态方法getDefaultToolkit得到一个Toolkit对象,这个对象中的getScreenSize方法以Dimension对象保存屏幕大小信息。该Dimension对象中的height和width实例字段保存了高度的宽度信息。
Toolkit tool = Toolkit.getDefaultToolkit();
Dimension screen = tool.getScreenSize();
int height = screen.height;
int width = screen.width;
frame.setSize(height / 2, width / 2);

此外,可以提供窗体图标:

Image img = new ImageIcon("icon.gif").getImage();
setIconImage(img);

添加组件

图形界面的组件被放在JComponent的类中,要创建自己的组件时,需要先创建一个继承了JComponent的类,并覆盖其中的paintComponent方法,该方法将一个Graphics对象作为参量:

class SimpleComponent extends JComponent {
    public void paintComponent(Graphics g) {
        g.drawString("我不知道", 100, 100);
    }
}

drawString方法用于打印字符串。

处理图形

我们将使用Java 2D图形库绘制图形,需要获得Graphics2D的一个对象。这个类是Graphics的一个子类,要使用2D,需要进行强制类型转换: Graphics2D g2 = (Graphics2D) g 可以使用Graphics2D对象的方法来处理图形。