关于 Java

  1. Java 既是编译型的,又是解释性的。原始 Java 代码经过编译之后转换为一种中间字节码,Java 虚拟机(JVM)可以将字节码解释执行。
  2. 目前的 Java 版本有三种:Java SEJava EEJava ME
    • Java SE 是 Java 的标准版,主要用于桌面应用程序的开发,同时也是 Java 的基础,它包含 Java 语言基础、JDBC 操作、I/O、网络通信、多线程等技术。
    • Java EE 是 Java 的企业版,主要用于开发企业级分布式网络程序,其核心为 EJB(企业 Java 组件模型)。
    • Java ME 主要应用于移动应用开发及嵌入式开发。
  3. Java 是纯面向对象语言,支持面向对象的三大特征:封装继承多态
  4. 相比于 C++,Java 拥有极其丰富的类库,使用这些类库可以很容易地开发复杂的应用程序。
  5. CLASSPATH 环境变量:当使用 java 类名 来运行一个 java 程序时,JRE 会去 CLASSPATH 指定的目录寻找这个 java 类,所以 CLASSPATH 中要包含 . 表示当前路径,除此之外,编译和运行 java 程序还需要 JDK 的 lib 目录下 dt.jartools.jar 文件中的 java 类,因此还需要将这两个文件添加到 CLASSPATH 中。其实,在使用 jdk 1.4 及以下的 jdk 版本时,CLASSPATH 是必须要设置的,而其他版本已经不需要设置这个变量了。

Java 程序的结构

  • Java 是一种纯面向对象的语言,Java 程序的基本组成单元是类,类体中又包含属性和方法。类名首字母必须为大写,这是强制性的规定,而不是约定。所以 Java 的类名一般采用大驼峰式命名法,这是约定。
  • 通常情况下,一个 Java 文件的文件名可以是任意的,但有一种情况例外:如果 Java 文件中定义了一个 public 类,则该文件的文件名必须与该 public 类名相同。
  • 由于 Java 文件的文件名必须与 public 类的类名相同,因此,虽然一个 Java 文件中可以定义多个类,但最多只能定义一个 public 类。
  • 可以使用将功能相近的 Java 类文件组织起来,包就相当于一个目录。包名约定为全小写
  • 如需某个类能被解释器直接执行,这个类必须包含一个 main 方法,main 方法是 Java 程序的入口。含有 main 方法的类叫做主类。main 方法的修饰符必须是 public static void,形参必须是 String[] args,也就是说,main 方法的写法几乎是固定的。
  • 除了主类之外的类不需要包含 main 方法,因为对于一个大型的 Java 应用程序而言,往往只需要一个入口,也就是只有一个类包含 main 方法,而其他类都是用于被 main 方法直接或间接调用的。

下面是一个最简单的 Java 程序:

1
2
3
4
5
6
7
8
package main;	// 包声明
import java.util.Hashset; // 导包

public class Hello { // 类定义
public static void main(String[] args) { // 定义 main() 方法
System.out.println("Hello Java!");
}
}
  1. 第一行声明了这个 Java 文件是属于 main 包的。
  2. 第二行是一个导包操作,使用 import 语句可以将其他地方的类导入当前类以使用它的代码。
  3. 第四行定义了一个 public 类,类名是 Hello,所以这个 Java 文件的文件名也必须是 Hello。
  4. 第五行定义了 main 方法,这是这个 Java 程序的入口。

注释与文档

相较于 C++,Java 多了一个文档注释的概念。文档注释包含在 /** */ 中,用法与多行注释 /* */ 相似。但不同的是,使用 javadoc 命令可以为指定的 Java 文件或包生成 API 文档,此时文档注释会被 javadoc 读取以作为 API 文档的内容。注意:文档注释必须写在声明语句(包括类声明、变量声明或方法声明等)之前,才会被 javadoc 读取。

javadoc 命令的使用方法:

1
$ javadoc 选项 java文件/包

常用选项:

  • -d <directory>:指定生成的文档所存放的路径
  • -windowtitile <text>:设置文档显示在浏览器中时的标题。
  • -doctitile <html>:设置概述页面的标题,是一段 html 格式的文本(只有对处于多个包下的源文件来生成 API 文档时,才会有概述页面)

如果需要生成更详细的文档信息,例如方法的参数、返回值等,可以在源程序中使用 javadoc 标记,将需要提取的信息写在标记后面。

常用的 javadoc 标记:

标记 说明
@author 指定 java 程序的作者
@version 指定程序的版本
@deprecated 不推荐使用的方法
@param 方法的参数说明信息
@return 方法的返回值说明信息
@see “参见”,用于指定交叉参考的内容
@exception/@throws 抛出的异常

注意:javadoc 默认不会提取 @author@version 的内容,如果需要提取,要在使用 javadoc 命令时加上 -author-version 参数,如:

1
$ javadoc -author -version test.java

变量与常量

  • 变量名的命名规则与 JavaScript 相同,可以使用美元符($)和中文

  • 常量的定义需要加 final 关键词:final int A = 1;

基本数据类型

整数类型

数据类型 占用内存
byte 1 字节(8位)
short 2 字节(16位)
int 4 字节(32位)
long 8 字节(64位)

在通常情况下,直接给出一个整数值默认是 int 类型,因此,有如下两种情况需要注意:

  • 如果直接将一个较小的整数值(在 byteshort 类型的数值范围内)赋值给一个 byteshort 类型的变量,Java 会把这个整数值当做 byteshort 类型来处理。
  • 如果将一个较大的整数值(超出了 int 的数值范围,但未超出 long 的数值范围)赋值给 long 型变量,Java 不会将它当做 long 类型,所以这种操作会导致语法错误。在这个整数值后面加 lL(推荐使用 L,因为 l 容易与数字 1 搞混),则这个整数不再是默认的 int 型,而是 long 型,此时再将它赋值给 long 型变量就不会出问题了。

注意:上面的 “当做” 并不是 “隐式类型转换”。

1
2
3
4
5
6
7
byte a = 56;	// ok。56 默认是 int 型的 56,这里被 Java 当做 byte 型的 56,然后赋值给 a。也就是说,这句话里并不存在 隐式类型转换。

long b = 9999999999999; // not ok。9999999999999 默认是 int 型,Java 不会把它当做 long 型来处理。由于 9999999999999 超出了 int 型的数值表示范围,所以会引起错误。
long c = 9999999999999L; // ok。9999999999999L 本来就是 long 型,而且没有超出 long 型的数值表示范围,所以这里不会报错。
long d = 99999; // ok。99999 默认为 int 型,且没有超出范围,它被隐式转换为 long 型后赋值给 d。

/* 只有最后一句存在隐式类型转换,其他都不存在隐式类型转换 */

浮点类型

数据类型 占用内存
float 4 字节(32位)
double 8 字节(64位)
  • Java 中的浮点类型默认是 double,如果希望 Java 能把一个浮点数当做 float 来处理,应该在该浮点数后面加上 fF。所以 float a = 1.2; 这种写法会报语法错误,而 float a = 1.2f; 这种写法是正确的。
  • Java 的浮点数遵循 IEEE 754 标准,采用二进制数据的科学计数法来表示浮点数。对于 float 型数值,第 1 位是符号位,接下来 8 位表示指数,最后 23 位表示尾数;对于 double 型数值,第 1 位是符号位,接下来 11 位表示指数,最后 52 位表示尾数。
  • 由于浮点数使用二进制数据的科学计数法来表示,因此可能不能精确表示一个浮点数。使用 double 类型来表示一个浮点数会比 float 类型更精准,但如果要表示的浮点数的位数很多,依然会发生这种情况。如果开发者需要精确保存一个浮点数,可以考虑使用 BigDecimal 类。
  • Java 还提供了三个特殊的浮点数值:正无穷大负无穷大非数,用来表示溢出出错。使用一个正浮点数除以 0 会得到正无穷大;使用一个负浮点数除以 0 会得到负无穷大;使用 0.0 除以 0.0 会得到非数。需要指出的是:所有的正无穷大的值都是相等的,所有负无穷大的值也都是相等的,而 NaN 不与任何数值相等,甚至和 NaN 都不相等。另外,只有浮点数除以 0 才能得到正无穷大和负无穷大,而整型数除以 0 会报错。

字符类型

Java 中的字符用 char 关键词声明,这与 C++ 相同,但由于 Java 使用 16 位的 Unicode 字符集作为编码方式,所以 Java 的字符占 2 个字节,可以存储汉字。

布尔类型

boolean 定义布尔类型,布尔类型只有两种值:truefalse。一般情况下,boolean 类型的变量占 1 个字节的空间。

引用类型

除了以上四种基本数据类型外,其他类型(如数组、类、接口等)都属于引用类型。在传参时,基本数据类型是值传递,而引用类型是引用传递

数值的表示

  • Java 中整数类型有 4 种表示方式:十进制、二进制、八进制、十六进制。二进制以 0B0b 开头;八进制以 0 开头;十六进制以 0X0x 开头。

  • Java 中浮点数可以用科学计数法的形式表示:5.12e2/5.12E2 –> $5.12 \times 10 ^ 2$

  • 从 jdk1.7 开始,开发者可以在数值中使用下划线,防止数值位数过多时“看花了眼”。

    1
    2
    int vinVal = 0B1000_0000_1111_0101;
    double pi = 3.14_15_926;
  • 数值在 Java 代码中以原码的形式存在,而在计算机内部以补码的形式存在。也就是说,当输入和输出一个数时它是原码的形式,当对这个数进行运算时它是补码的形式。举个例子:

    1
    2
    3
    int a = 0B0000_0000_0000_0000_0000_0000_1000_0001;
    int b = ~a; // 1111_1111_1111_1111_1111_1111_0111_1110
    System.out.println(b); // 1000_0000_0000_0000_0000_0000_1000_0010

    a 首先被赋值为 0000_0000_0000_0000_0000_0000_1000_0001(129),这是原码形式,在计算机内部原码被转换为补码形式存在,因为这个数是正数,所以补码与原码相同,仍然是 0B0000_0000_0000_0000_0000_0000_1000_0001。之后对 a 的补码进行取反操作,得到 1111_1111_1111_1111_1111_1111_0111_1110,也就是 b 的补码形式。最后输出 b 时,要将补码形式转换为原码形式,即 1000_0000_0000_0000_0000_0000_1000_0010(-130)。

类型转换

隐式类型转换

  • 当将一个低级类型的数据赋值给高级类型时,该数据会被隐式类型转换。

    转换规则:

    • byte -> short -> int -> long -> float -> doubledouble 的等级最高。

    • char 可以转换为以上类型中除 byte 以外的其他类型。

  • 当一个表达式中存在多种数据类型时,整个表达式的数据类型会自动提升到与表达式中最高等级操作数同样的类型:

    1
    2
    short value = 5;
    value = value - 2; // 这会发生错误,因为 2 是 int 型,该表达式提升为 int 型,导致运算结果也是 int 型,将 int 型的值赋值给 value 这个 short 型的变量就会出现错误。
  • Java 中的 boolean 类型只能有两个值:truefalse,且无法与任何基本类型相互转换,所以,下面几个在 C++ 中能行得通的写法在 Java 中并不适用:

    1
    2
    3
    4
    5
    int a = true;	// 错,因为 boolean 无法转换为整型

    if (1) { // 错,因为整型无法转换为 boolean 类型,所以这里无法使用整型数作为判断条件
    // some code
    }
  • 当把任意基本数据类型的值与字符串做连接运算时,基本类型的值会自动转换为字符串类型。所以,如果需要将一个基本数据类型的值转换为字符串,可以把它和一个空字符串连接。

强制类型转换

  • Java 中强制类型转换的写法与 C++ 相同。

  • 将浮点数强制转换为整型数时,小数位会被直接截断。

  • 将占用字节数较少的类型转换为占用字节数较多的类型时可以正常转换,但将占用字节数较多的类型转换为占用字节数较少的类型时,超出范围的高位(二进制下)会被截断,导致数据发生变化,如:

    1
    2
    3
    short i = 129;
    byte b = (byte)i;
    System.out.println(b); // output: -127

    129 的二进制形式是 0000000010000001,正数的补码与原码相同,所以转换为补码后仍然是 0000000010000001,将它转换为 byte 类型后,前八位会被截断,只剩下 10000001,当输出时,再将补码换算成原码,也就是将后七位减一并取反,得到原码 11111111,所以最后输出的结果为 -127。

  • 通常情况下,字符串不能直接转换为基本数据类型,但通过基本类型对应的包装类中的 parseXxx 方法可以将字符串转换为基本类型。

运算符

注:数值在计算机内部进行运算时,都是采用补码形式。

  1. 算数运算符、自增自减运算符、比较运算符、赋值运算符和三目运算符都与 C++ 相同。

  2. 位运算符:

    一般来说,位运算符只能操作整数类型的值和变量。

    Java 中的位运算符有 7 个:

    位运算符 说明
    & 按位与
    ` `
    ~ 按位非
    单目运算符,将操作数的每个位(包括符号位)全部取反
    示例见数值的表示中的最后一点
    ^ 按位异或
    当两位相同时返回 0,不同时返回 1
    << 左移运算符
    将操作数的二进制码整体左移指定位数,左边截断,右边空出来的以 0 填充
    >> 右移运算符
    将操作数的二进制码整体右移指定位数,左边空出来的以 原来的符号位 填充
    >>> 无符号右移运算符
    将操作数的二进制码整体右移指定位数,左边空出来的以 0 填充

    进行移位运算时还要遵循以下规则:

    1. 对于低于 int 类型(如 byteshortchar)的操作数总是先自动转换为 int 型后再移位。
    2. 对于 int 类型的整数移位 a >> b,当 b > 32 时,系统先用 b 对 32 求余(因为 int 类型是 32 位),得到的结果才是真正移位的位数。例如,a >> 33a >> 1 结果相同。
    3. 对于 long 类型的整数移位 a >> b,当 b > 64 时,系统先用 b 对 64 求余(因为 long 类型是 64 位),得到的结果才是真正移位的位数。

    当进行移位运算时,只要被移位的二进制码没有发生有效位的数字丢失,不难发现左移 n 位就相当于乘以 $2^n$,右移 n 位则是除以 $2^n$。

  3. 扩展后的赋值运算符:

    赋值运算符可以与算数运算符、位运算符结合,成为扩展后的赋值运算符。

    +=:对于 x += y,相当于 x = x + y

    这样的运算符还有:-=*=/=%=&=|=^=<<=>>=>>>=

  4. 逻辑运算符:

    • 逻辑运算符有:&&||!&|^
    • 逻辑运算符只能操作 boolean变量或常量。
    • &&||! 与 C++ 相同。 &&|| 具有短路效应
    • Java 多了个不短路与 & 和 不短路或 | 操作符。顾名思义,使用这两个运算符没有短路效应
    • ^:异或,当两个操作数不相同时返回 true,相同时返回 false
    • 可以看到,&|^ 既可以是位运算符,也可以是逻辑运算符,那么如何判断它们是位运算符还是逻辑运算符呢?当它们的操作数是整型数时,它们是位运算符;当它们的操作数是 boolean 型时,它们是逻辑运算符。

流程控制

  1. Java 中的 if-else 语句、switch-case 语句、whiledo-while 用法都与 C++ 相同。for 循环语句、breakcontinue 的基本用法也与 C++ 相同。

  2. 从 Java7 开始,switch-case 语句的表达式可以是一个 String 类型的字符串(但不能是 StringBufferStringBuilder 类型)。

  3. Java 中的 for 循环多了一种写法,这种写法叫做 foreach 语句,使用 foreach 语句可以方便地遍历数组:

    1
    2
    3
    4
    int array[] = { 7, 10, 1 };
    for (int item : array) {
    System.out.println(item);
    }
  4. 在 C++ 中,break 语句无法跳出嵌套的多层循环,Java 解决了这个问题:

    1
    2
    3
    4
    5
    6
    7
    8
    outer: 	// 为外层循环设置标签,标签名为 outer
    for (int i = 0; i < 10; i++) {
    for (int j = 0; j < 10; j++) {
    System.out.println(i * j);
    if (j == 4)
    break outer; // 跳出标签名为 outer 的循环(即外层循环)
    }
    }

    break 语句一样,continue 也支持标签功能。

    注意:标签必须放在循环语句之前才有作用。

数组

数组的定义与初始化

数组定义有两种方式:

1
2
type[] array;
type array[]; // C++ 中的写法

​ Java 中常采用第一种写法,因为它具有更好的语义:type[] 是一种数据类型,使用它可以定义一个引用,这个引用指向一个 type 类型的数组。

数组的初始化有两种方式:

  1. 静态初始化:

    初始化时由开发者显式指定每一个元素的值,由系统自动计算数组长度。

    1
    2
    int[] array = {1, 2, 3};
    int[] array = new array[] {1, 2, 3};

    两种写法都可以。

  2. 动态初始化:

    开发者只指定数组长度,由系统为数组元素分配初始值。

    1
    int[] array = new int[length];

    数组元素的初始值:

    数组类型 数组元素的初始值
    整型(longintshortbyte 0
    浮点型(doublefloat 0.0
    字符类型(char \u0000
    布尔类型(boolean false
    引用类型(数组、类、接口等) null

多类型数组

存储在数组中的元素必须具有同样的数据类型,但由于 Java 是面向对象的语言,而类与类之间可以支持继承关系,所以用父类类型定义的数组可以存放父类类型及其所有子类类型的变量,如:

1
2
3
4
Object[] array = new Object[3];	// 在 Java 中,Object 类是所有类的基类,包括基本数据类型
array[0] = "Hello world";
array[1] = 1;
array[2] = 1.2;

多维数组

在 Java 中可以方便地定义多维数组:

1
2
3
4
int[][] a = new int[3][4];	// 定义二维数组
int[][][] b = new int[3][4][5]; // 定义三维数组

// 以此类推。。。不管多少维都可以定义

虽然 Java 语法支持定义多维数组,但是在计算机内部实际上是没有多维数组的,下面以二维数组为例,阐述 Java 定义多维数组的原理:

上面已经指出,int[] 也是一种数据类型,用它定义的变量是一个引用,这个引用指向一个 int 型的数组。那么定义二维数组也是一样的,int[][] 中的 int[] 是一种引用类型,在它后面再加 [] 表示:定义一个引用,这个引用指向一个 int[] 型的数组。

下图是一个二维数组在内存中的存储示意图,a 是一个 int[][] 型的引用,它指向一个 int[] 型的数组,数组中每个元素都是一个 int[] 型的引用,每个引用又指向一个 int 型的数组。

二维数组在内存中的存储

由此也可以推出二维数组静态初始化的方法:

1
int[][] a = new int[][] {new int[] {1, 2, 3}, new int[] {1, 2, 3}};

这样可以得到一个两行三列的二维数组。

使用工具类 Arrays 操作数组

为了更方便地操作数组,Java 提供了 java.util.Arrays 工具类。

该类提供了很多用于操作数组的静态方法,如:复制、排序、搜索、判断是否相等、填充、转换为字符串 等。

类和对象基础

类的定义

定义类:

1
2
3
4
5
[修饰符] class 类名 {
构造函数
成员变量
方法
}
  • 修饰符可以省略,也可以是 publicfinalabstract 中的一个或多个。
  • 类名使用 大驼峰命名方式
  • 如果没有定义构造函数,系统会提供一个无参的默认构造函数。

定义构造函数:

1
2
3
[修饰符] 构造函数名(形参列表) {
方法体
}
  • 修饰符:可以省略,也可以是 publicprotectedprivate 其中之一。
  • 构造函数名:必须与类名相同
  • 构造函数没有返回值,不需要写返回值类型,void 也不行。

定义成员变量:

1
[修饰符] 类型 成员变量名 [= 默认值];
  • 修饰符:可以省略,也可以是 publicprotectedprivatestaticfinal,其中,publicprotectedprivate 只能出现其中之一,可以与 staticfinal 组合起来修饰成员变量。
  • 成员变量名:使用 小驼峰命名方式

定义方法:

1
2
3
[修饰符] 返回值类型 方法名(形参列表) {
方法体
}
  • 修饰符:可以省略,也可以是 publicprotectedprivatestaticfinalabstract,其中,publicprotectedprivate 只能出现其中之一,abstractfinal 只能出现其中之一,它们可以与 static 组合起来修饰方法。
  • 方法名:使用 小驼峰命名方式,并建议以动词开头。

static 修饰符

  • static 修饰的成员变量和方法称为 类变量(或静态变量)、类方法(或静态方法)。

  • 在 C++ 中,静态方法与类方法并不是同一个概念,而 Java 中是。

  • 静态成员只能访问静态成员,不能直接访问普通成员,访问普通成员需要先创建一个类的实例,再通过实例访问。

  • static 修饰的成员变量和方法,需要通过类来调用,而普通成员变量和方法则通过对象来调用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class Person {
    public String name = "xxx"; // 成员变量

    public String getName() { // 普通方法
    return this.name;
    }
    public static void say(String sentence) { // 静态方法
    System.out.println(sentence);
    }
    public static void main(String[] args) { // main 方法必须用 static 修饰
    say("Hello"); // 静态方法中可以直接调用另一个静态方法

    Person p = new Person();
    System.out.println(p.getName()); // 静态方法中调用非静态方法必须先创建一个对象,再通过对象调用
    }
    }

对象的产生和使用

1
Person p = new Person();
  • 等号左边定义了一个 Person 类型的引用 p
  • 等号右边使用 new 关键字实例化一个 Person 对象,实例化时会自动调用匹配的构造函数完成初始化操作。
  • 将右边新创建的对象赋值给左边的引用,该引用就指向了该对象,此后可以通过引用来调用对象的成员变量和方法。
1
2
p.name = "xxx";	// 访问成员变量
p.getName(); // 调用方法

关于引用

  • Java 中的引用实际上就相当于 C/C++ 中的指针,只是 Java 把这个指针封装了起来,避免开发者进行繁琐的指针操作。
  • 当没有引用指向一个对象时,这个对象就会自动被 Java 的垃圾回收机制回收掉,释放其所占的内存空间。

this 关键字

  • Java 提供了一个 this 关键字,它总是指向 当前对象

  • static 修饰的方法不能使用 this 关键字。

  • 在类的一个方法中访问该类里的另一个方法或变量时,要用到 this 关键字,通过 this.xxx 的方式访问。

    大部分情况下,this 关键字可以省略,但这实际上只是一种假象,虽然 this 被省略了,但实际上这个 this 仍然是存在的。

    如果方法里的局部变量与类的成员变量同名,默认使用的是局部变量,如果要使用成员变量,必须用 this.xxx 的方式使用,不能省略 this

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class Person {
    public String name = "xxx";
    public int age = 20;

    Person(String name, int age) {
    this.name = name; // name 是局部变量,this.name 是成员变量
    this.age = age;
    }
    }
  • this 也可以作为返回值返回。

形参个数可变的方法

在 JDK1.5 之后,Java 允许定义形参个数可变的方法(在最后一个形参的数据类型后加三个点,多个参数会被当做数组传入):

1
2
3
4
5
6
7
8
9
10
11
public class Varargs {
public static void test(int a, String... books) {
for (String tmp:books) {
System.out.println(tmp);
}
}

public static void main(String[] args) {
test(4, "a", "bb", "ccc");
}
}

可变参数本质上是一个数组参数,也就是说,下面两种方法定义的效果是一样的:

1
2
public static void test(int a, String... books);
public static void test(int a, String[] books);

但在调用方法时,可变参数可以有两种传参方式:

1
2
test(5, 'a', 'bb', 'ccc');
test(5, new String[] {'a', 'bb', 'ccc'});

而数组参数只能传数组:

1
test(5, new String[] {'a', 'bb', 'ccc'});

类的继承

多态

Java 基础类库

Java 集合

泛型

异常处理

注解

输入输出流

多线程

网络编程

字符串

技巧

当把任何基本数据类型的值与字符串做连接运算时,基本类型的值会自动转换为字符串类型。所以,如果需要将一个基本数据类型的值转换为字符串时,可以把它和一个空字符串连接。