去年的人工智能导论中就已经接触过Java,在那之前还学习过一段时间,但随着时间推移内容已经忘了不少,只记得基础的语法,今年的$KR\&P$还是要用到Java,故在学期初期事情比较少的时候进行学习和整理。同时,作为一门流行至今的程序设计语言,会总比不会好。
自从学习过《计算机系统基础》之后,对程序设计语言的了解不再满足于仅仅编写出Hello World,而是想知道程序的运行过程。
我们来看看Java的编译和运行过程:

Java程序既是编译型,又是解释型的:
程序代码经过编译之后转换为称作Java字节码的中间语言;
Java虚拟机(JVM)对字节码进行解释(解释成机器码)和运行。
编译一次但解释在每次运行程序时都会进行。
明白这一过程,我们就可以弄清JDK和JRE的区别了:
JRE : Java runtime environment,它提供了Java的运行环境,也就是说,它只能运行编译好的字节码,而不能让程序被编译成字节码,有了JRE我们就可以运行Java程序,比如Logisim;
JDK : Java development kit,是程序员使用java语言编写java程序所需的开发工具包,是提供给程序员使用的。JDK包含了JRE,同时还包含了编译java源码的编译器javac,还包含了很多java程序调试和分析的工具:jconsole,jvisualvm等工具软件,还包含了java程序编写所需的文档和demo例子程序。——摘自知乎问答
这里,我们将不再依赖一键编译运行工具,例如code-runner,一些IDE等去运行一个Hello World,而是通过自己的理解去用命令行写出来:
public class Main {
public static void main(String[] args) {
System.out.println("Hello world");
}
}
我们已经知道,javac就是前面所说的编译器,而java是解释器:
>> tldr java
- Execute a java .class file that contains a main method by using just the class name:
java
...
>> tldr javac
- Compile a .java file:
javac
...
那么我们需要做什么?先编译后运行:
>> javac Main.java # 会生成Main.class文件
>> java Main # 运行的是Main这个类
Hello world
运行成功!
该部分着重强调的是Java语法与C/C++,Python语法的区别和联系,便于笔者记忆和学习,建议有一定C/C++语言基础的读者观看。
八进制表示:以0开头,0123就是十进制的83;
多出一个byte类型表示int8;
小数默认被看做double,如果想要是float就要在后面加上f或者F
float f1 = 13.23f;
double d1 = 4562.12d;
double d2 = 45678.1564;
Java里的char占两个字节,是C/C++的两倍,使得java的字符几乎可以处理所有国家的语言文字:
我们若在C中运行下面的程序:
int main() {
char c = '你';
printf("%c\n", c);
return 0;
}
程序会报错,因为此时字符‘你’不在ASCII表内,属于overflow;
但在Java中:
public static void main(String[] args) {
char c = '你';
System.out.println(c);
}
程序会正确输出‘你’,这就是差异。
boolean,而不是C++中的bool,但两个字面量true和false是相同的。Java中我们用const关键字声明一个常量,而且常量是否为成员变量会影响其用法:
public class Main {
static final int member_final = 1;
public static void main(String[] args) {
final int local_final;
local_final = 2;
System.out.println(local_final);
}
}
如果member_final(成员常量)只声明不赋值,那么会报错;
如果local_final(局部常量)只声明不赋值是可以的,但之后必须被赋值,也只能被赋值一次,否则报错;
Java中自增自减运算符存在,用法与C/C++相同;
Java中移位操作多出一个>>>,意思是无符号右移。
Java也有switch-case,语法与C相同,但多出对字符串类型的支持;
Java有和C++类似的基于范围for循环:
int arr[] = {7, 10, 1};
for (int x : arr) {
System.out.println(x);
}
常用的字符串构造方法:(字符串使用前必须经过初始化,否则报错)
用字符数组初始化
char a[] = {'w', 'e', 'l', 't'};
String s = new String(a);
提取字符数组初始化
char a[] = {'w', 'e', 'l', 't'};
String s = new String(a, 1, 2); // 偏移1,取2个,也就是"el"
字符串常量的引用赋值
String s1, s2;
s1 = "welt";
s2 = "welt";
这里是s1和s2指向同一段内存空间,该内存空间存储的是字符串”welt”。
String之间可以相互连接,用’+’即可;此外,还可以连接其他数据类型,过程是调用toString()方法,然后再连接;
字符串查找:indexOf(String s)找子串首次出现的索引位置,没找到就返回-1,lastIndexOf类似,但找的是最后一次出现的子串;
获取String指定索引位置的字符不能像C++那样直接用索引,而是str.charAt(int index);
*
1. 用于获取子串的subString(int begin_index), subString(int begin_index, end_index);
2. 用于去除空格的trim();
3. 用于字符串替换的replace(char old, char new);
4. 用于判断字符串开头和结尾的startWith(String prefix)和endWith(String suffix);
5. 判断字符串相等不能用”==”,和C中原因几乎相同,equals(Stirng)方法返回boolean,compareTo(String)方法返回int,相当于C中的strcmp(char*);
6. 用于分割字符串的split,split(String sign),sign为分割字符串的分割符,也可以用正则表达式;split(String sign, int limit)则限定了拆分次数,会分割limit-1次,也就是limit个字符串。
由于本人很少使用日期时间的原因,故关于日期和时间的格式化在这里跳过。
相比于C\C++,Java的常规类型格式化多出这些内容:
"%b, %B",格式化为布尔类型;
"%h, %H",格式化为散列码;
"%a, %A",格式化为带有效位和指数的十六进制浮点数。
正则表达式,Java兼容了基本的正则表达式:
public static void main(String[] args) {
Scanner s = new Scanner(System.in);
String str;
String regex = "\\w+@\\w+(\\.\\w{2,3})*\\.\\w{2,3}";
do {
str = s.next();
if (str.equals("exit")) {
break;
} else if (str.matches(regex)) {
System.out.println(str + "是一个合法的邮箱地址格式");
} else {
System.out.println(str + "不是一个合法的邮箱地址格式");
}
} while (true);
s.close();
return;
}
该程序是一个邮箱地址格式判断的程序,其中的regex就是一个正则表达式。
简单的说,就是String的内容无法改变,导致在进行连接的时候其实是多出一个copy,如果操作频繁,回导致效率很低,所以有一个新的类StringBuilder,一个可变的字符序列,它的几个常用方法:append()可以追加内容,参数可以是多个类型;insert(int offset, String content)将在offset处插入content字符串;delete(int start, int end)删除start和end之间的字符串。
你可以这样创建一个一维数组:
ElemType ArrName[];
ElemType[] ArrName;
这样的数组是无法使用的,你还需要为其分配内存空间:
ArrName = new ElemType[ElemNumber];
使用new分配数组内存时,整形数组中所有元素都是0。
当然,上面的操作可以合并进行:
ElemType ArrName[] = new ElemType[ElemNumber];
ElemType[] ArrName = new ElemType[ElemNumber];
该写法是普遍采用的创建数组方法。
数组的初始化有以下两种基本形式:
int arr1[] = new int[]{1, 2, 3};
int arr2[] = {4, 5, 6, 7};
你可以这样先声明再分配一个二维数组:
ElemType ArrName[][];
ElemType[][] ArrName;
分配空间的时候可以一起分配或者分别分配:
a = new int[2][4]; // 一起分配
a = new int[2][];
a[0] = new int[3];
a[1] = new int[4]; // 分别分配
二维数组的初始化与一维数组类似,同样可以使用大括号完成:
int arr[][] = { {1, 2}, {3, 4} };
使用new分配二维数组数组内存时,整形数组中所有元素都是0。
最基础的遍历:
for (int i = 0; i < arr.length(); i++) {
for (int j = 0; j < arr[i].length(); j++) {
TODO();
}
TODO();
}
利用foreach遍历:
for (int x[] : arr) {
for (int e : x) {
TODO();
}
TODO();
}
fill(ElemType[] a, ElemType value)将数组a中所有元素填充为value;
fill(ElemType[] a, int formIndex, int toIndex, ElemType value)是有范围的填充;
public static void main(String[] args) {
int[] arr = { 1, 6, -1, 7 };
Arrays.sort(arr);
for (int e : arr) {
System.out.print(e + " ");
}
System.out.println();
}
int arr[] = {1, 2, 3, 4, 5};
int arr_copy[] = Arrays.copyOf(arr, 3);
arr_copy长度为3,是arr的前3个元素,如果copyOf的第二个参数,也就是长度参数大于复制目标的长度,那么就用0填充。
范围复制:
int arr[] = {1, 2, 3, 4, 5};
int newarr[] = Arrays.copyOfRange(arr, 0, 3);
虽然C++和Java共用一套权限修饰符,但含义有些许差别。
| private | protected | public | |
|---|---|---|---|
| 本类 | 可见 | 可见 | 可见 |
| 同包的其他类或子类 | 不可见 | 可见 | 可见 |
| 其他包的类或子类 | 不可见 | 不可见 | 可见 |
包的概念将在后面介绍。
特点,没有返回值(和C++相同),而且不需要void关键字修饰。
如果在类中定义的构造方法都不是无参的,那么编译器也不会为其设置一个默认构造函数,只有在类中没有定义任何构造方法时,系统才会自动创建默认构造函数。
this也可以调用构造方法:
public Anything() {
this("this 调用有参构造方法");
System.out.println("无参构造方法");
}
public Anything(String name) {
System.out.println("有参构造方法");
}
由static关键字修饰的变量,常量和方法被称作静态变量、常量和方法。
作用是为了”共享”(和C中的static的作用“相反”);我们直接使用类名.静态成员/方法就可以使用。
静态方法的规定:
静态方法中不能使用this关键字;
在静态方法中不能直接调用非静态方法。
Java中不允许方法体内的局部变量为
static。 如果在执行类的时候希望先执行类的一些初始化动作,可以使用static定义一个静态区域:public class example { static { TODO(); } }
形式:
public static void main(String[] args) {
TODO();
}
可以看出:
主方法是静态的,所以如果要在主方法中调用其他函数,这些函数也必须是静态的;
和C不一样,这里的main是没有返回值的;
主方法的形参就是参数,我们可以用程序测试一下:
public static void main(String[] args) {
for (String arg : args) {
System.out.println(arg);
}
}
在命令行输入:
>> javac Main.java # 此处不是输入参数的地方,因为不执行
>> java Main 1 2 3
然后就可以看到输出:
1
2
3
在之前的数组和字符串部分,我们已经在创建对象了:
String str = new String("abc");
在此强调这里的语法和C++的差别:
A* a = new A(...);
C++的new语法创建的是一个指针,但Java创建的就是一个对象的引用。
JVM的内存处理机制:每个对象都有生命周期,生命周期结束后就会被回收,无法再被使用。
事实上,在
String str = new String();
中,标识符str称作“引用对象”,如一个Book类的引用可以使用以下代码:
Book book;
一个引用对象不一定需要有一个对象相关联(比如上面),引用与对象相关联的语法如下:
Book book = new Book();
引用只是存放对象的内存地址,并非存放一个对象。严格地说,引用和对象不同。
前桥和弥在《征服C指针》中提到过一个趣闻:JAVA在推出时宣传自己是没有指针的,目的就是为了吸引当时被C指针折磨已久的程序员们。但实际上前桥认为Java的引用可以说就是C的指针,所以这算是一个“虚假营销”。至此我们可以发现他的观点确实有合理之处。
Java中有两种比较对象的方式:分别为“==”运算符和equals()方法:
object1 == object2; // 比较的是两个对象引用的地址是否相同;
object1.equals(object2) // 比较的是两个对象引用所指的内容是否相同.
区别可以理解为C中两个char*,ptr1和ptr2,==比较的就是地址是否相同,但strcmp(ptr1, ptr2)比较的是内容。
对象的销毁和我们之前提到的JVM垃圾回收机制相关,我们先来看下什么样的对象会被JVM识别为垃圾:
对象引用超过了作用域:
{
A a = new A();
}
// 到了这里A就应该消亡了
将对象赋值为null:
A a = new A();
a = null;
JVM只能回收由new操作符创建的对象,否则无法被其识别,此时你可以自定义Object的finalize方法,类似与C++中的重载delete.
参见Java异常处理
Java中所有输入流都是抽象类InputStream(字节输入流)或者抽象类Reader(字符输入流)的子类。
Java中的字符是Unicode编码,是双字节的。InputStream是用来处理字节的,并不适合处理字节文本(如英文文档)。Java为字符文本的输入专门提供了一套单独的类Reader,但Reader类并不是InputStream类的替换者,只是在处理字符串时简化了编程。Reader类是字符输入流的抽象类。
Java中所有输出流都是抽象类OutputStream(字节输出流)或者抽象类Writer(字符输出流)的子类。同样,Writer类也是为了处理字符而单独提供的抽象类。
直接用程序说明:
File file1 = new File(filename);
File file2 = new File("dirname", "filename") // 比如dirname为"/home",filename为"test.txt"
你可以像下面这样进行更完备的文件创建:
File file = new File("word.txt");
if (file.exists()) {
file.delete(); // 文件已存在则将文件删除
} else {
try { // 文件不存在则创建新文件
file.createNewFile();
} catch (Exception e) {
e.printStackTrace();
}
}
以下是File类的方法,供查阅使用:
| 序号 | 方法描述 |
|---|---|
| 1 | public String getName() 返回由此抽象路径名表示的文件或目录的名称。 |
| 2 | public String getParent() 返回此抽象路径名的父路径名的路径名字符串,如果此路径名没有指定父目录,则返回 null。 |
| 3 | public File getParentFile() 返回此抽象路径名的父路径名的抽象路径名,如果此路径名没有指定父目录,则返回 null。 |
| 4 | public String getPath() 将此抽象路径名转换为一个路径名字符串。 |
| 5 | public boolean isAbsolute() 测试此抽象路径名是否为绝对路径名。 |
| 6 | public String getAbsolutePath() 返回抽象路径名的绝对路径名字符串。 |
| 7 | public boolean canRead() 测试应用程序是否可以读取此抽象路径名表示的文件。 |
| 8 | public boolean canWrite() 测试应用程序是否可以修改此抽象路径名表示的文件。 |
| 9 | public boolean exists() 测试此抽象路径名表示的文件或目录是否存在。 |
| 10 | public boolean isDirectory() 测试此抽象路径名表示的文件是否是一个目录。 |
| 11 | public boolean isFile() 测试此抽象路径名表示的文件是否是一个标准文件。 |
| 12 | public long lastModified() 返回此抽象路径名表示的文件最后一次被修改的时间。 |
| 13 | public long length() 返回由此抽象路径名表示的文件的长度。 |
| 14 | public boolean createNewFile() throws IOException 当且仅当不存在具有此抽象路径名指定的名称的文件时,原子地创建由此抽象路径名指定的一个新的空文件。 |
| 15 | public boolean delete() 删除此抽象路径名表示的文件或目录。 |
| 16 | public void deleteOnExit() 在虚拟机终止时,请求删除此抽象路径名表示的文件或目录。 |
| 17 | public String[] list() 返回由此抽象路径名所表示的目录中的文件和目录的名称所组成字符串数组。 |
| 18 | public String[] list(FilenameFilter filter) 返回由包含在目录中的文件和目录的名称所组成的字符串数组,这一目录是通过满足指定过滤器的抽象路径名来表示的。 |
| 19 | public File[] listFiles() 返回一个抽象路径名数组,这些路径名表示此抽象路径名所表示目录中的文件。 |
| 20 | public File[] listFiles(FileFilter filter) 返回表示此抽象路径名所表示目录中的文件和目录的抽象路径名数组,这些路径名满足特定过滤器。 |
| 21 | public boolean mkdir() 创建此抽象路径名指定的目录。 |
| 22 | public boolean mkdirs() 创建此抽象路径名指定的目录,包括创建必需但不存在的父目录。 |
| 23 | public boolean renameTo(File dest) 重新命名此抽象路径名表示的文件。 |
| 24 | public boolean setLastModified(long time) 设置由此抽象路径名所指定的文件或目录的最后一次修改时间。 |
| 25 | public boolean setReadOnly() 标记此抽象路径名指定的文件或目录,以便只可对其进行读操作。 |
| 26 | public static File createTempFile(String prefix, String suffix, File directory) throws IOException 在指定目录中创建一个新的空文件,使用给定的前缀和后缀字符串生成其名称。 |
| 27 | public static File createTempFile(String prefix, String suffix) throws IOException 在默认临时文件目录中创建一个空文件,使用给定前缀和后缀生成其名称。 |
| 28 | public int compareTo(File pathname) 按字母顺序比较两个抽象路径名。 |
| 29 | public int compareTo(Object o) 按字母顺序比较抽象路径名与给定对象。 |
| 30 | public boolean equals(Object obj) 测试此抽象路径名与给定对象是否相等。 |
| 31 | public String toString() 返回此抽象路径名的路径名字符串。 |
目的是将In/OutputStream和File进行连接:
FileInputStream常用的构造方法如下:
FileInputStream(String filename);
FileInputStream(File file);
FileOutputStream类有与上面类似的构造方法,创建一个FileOutputStream对象时,可以指定不存在的文件名,但该文件不能被其他程序打开。
我们来看一个测试程序:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
String content = "我虽无意逐鹿,却知苍生苦楚\n";
FileOutputStream out = new FileOutputStream("test.txt");
out.write(content.getBytes());
out.close();
FileInputStream in = new FileInputStream("test.txt");
System.out.print(new String(in.readAllBytes()));
in.close();
}
}
这里我们实现了将一句话写入文件,然后读取文件并输出到控制台的程序。
这里的in/out是针对Java程序而言:将外部文件的内容引入源文件为“in”,反之为“out”.
背景:由于汉字在文件中只占用两个字节,如果使用字节流,读取不好会出现乱码现象,此时采用字符流Reader/Writer类即可避免这种情况。
FileReader顺序读取文件,只要不关闭流,每次调用read()方法就顺序地读取源中其余的内容,直到源的末尾或者流关闭。
我们再次用类似的程序做测试:
public class Main {
public static void main(String[] args) throws IOException {
String content = "我虽无意逐鹿,却知苍生苦楚\n";
FileWriter fw = new FileWriter("test.txt");
fw.write(content);
fw.close();
FileReader fr = new FileReader("test.txt");
char[] ret = new char[1024];
fr.read(ret);
System.out.print(new String(ret));
fr.close();
}
}
可以发现我们这里写入可以是直接的字符串,而读取也可以用字符流读取。