引言

本篇博文为 Java 基础阶段的常见概念及相关细节梳理,意在重学 Java 查漏补缺。
博文随时会进行更新,补充新的内容并修正错漏,该系列博文旨在帮助自己巩固扎实 Java 基础。
毕竟万丈高楼,基础为重,借此督促自己时常温习回顾。

计算机基础

存储单位

基本换算:1MB = 1024KB、1KB = 1024B、1B = 1byte = 8bit

bit(比特、位):计算机中的最小存储单位

byte(字节):计算机中基本存储单元

Java 基本常识

.java 源文件编程规则

一个 .java 文件中可以有多个 class 但 public 修饰的 class 只能有一个

JRE、JDK、JVM 之间的关系

  • JRE = JVM + Java 核心类库
  • JDK = JRE + Java 开发工具(javac、java、javadoc、…)

编译与运行

  • javac 编译 .java 源代码文件为 .class 字节码文件
  • java 运行 .class 字节码文件
    • javac A.java (生成 A.class)
    • java A (运行 A.class,不需要加 .class 后缀)

Java 基础

注释

单行注释:

// 单行注释

多行注释:

/*
多行注释
多行注释
*/

文档注释:

/**
*文档注释
*文档注释
*/

Java 关键字与保留字

命名时要避免使用关键字与保留字

关键字: 被 Java 赋予特殊作用的单词。关键字中所有字母均小写

  • 用于定义数据类型的关键字:class、interface、enum、byte、short、int、long、float、double、char、boolean、void
  • 用于定义流程控制的关键字:if、else、switch、case、default、while、do、for、break、continue、return
  • 用于定义访问权限修饰符的关键字:private、(缺省)、protected、public
  • 用于定义类、方法(函数)、变量修饰符的关键字:abstract、final、static、synchronized
  • 用于定义类与类之间关系的关键字:extends、implements
  • 用于定义建立实例及引用实例,判断实例的关键字:new、this、super、instanceof
  • 用于异常处理的关键字:try、catch、finally、throw、throws
  • 用于包的关键字:package、import
  • 其他修饰关键字:native、strictfp、transient、volatile、assert
  • *用于定义数据类型值的字面值:true、false、null

保留字:Java 现版本尚未使用,但以后版本可能会成为关键字使用:goto、const

标识符

Java 对各种变量、方法和类等要素命名时使用的字符序列称为标识符(凡是可以自己起名字的地方都叫做标识符)

定义合法标识符的规则:

  • 由数字、字母、下划线( _ )、或 $ 组成
  • 不可以用数字开头
  • 不可以使用关键字和保留字,但可以包含关键字和保留字
  • 严格区分大小写,长度无限制
  • 不能包含空格
  • 见名知义

标识符命名规范:

  • 包名:多单词组成时所有字母均小写(aabbcc)
  • 类名、接口名:多单词组成时,所有单词首字母大写(AaBbCc)
  • 变量名、方法名:多单词组成时,第一个单词首字母小写,其余单词首字母大写(aaBbCc)
  • 常量名:所有字母均大写,多单词组成时每个单词使用下划线( _ )连接(AA_BB_CC)

变量

  • 变量用于表示特定类型的数据
  • 变量声明告知编译器根据数据类型为变量分配合适的内存空间
  • 一个变量在使用前必须被声明和初始化
  • 方法中声明的变量在使用之前必须被赋值

注意

  • Java 中每个变量必须先声明后使用
  • 使用变量名来访问这块区域的数据
  • 变量的作用域:其定义所在的一对 {} 内
  • 变量只有在其作用域内才有效
  • 同一个作用域内,不能定义重名的变量(变量不可以重复声明)

分类(按数据类型)

Java 对于每一种数据都定义了明确的具体数据类型(强类型语言),在内存中分配了不同大小的内存空间

  • 基本数据类型:
    • 数值型(整数类型(byte、short、int、long)、浮点类型(float、double))
    • 字符型(char)
    • 布尔型(boolean)
  • 引用数据类型:
    • 类(class)
    • 接口(interface)
    • 数组array([])

分类(按变量在类中声明的位置)

  • 成员变量:实例变量(不以 static 修饰)、类变量(以 static 修饰)
  • 局部变量:形参(方法、构造器中定义的变量)、方法局部变量(在方法内定义)、代码块局部变量(在代码块内定义)

基本数据类型

整型

Java 各整数类型有固定的表数范围和字段长度,不受具体的操作系统(OS)的影响,以保证 Java 程序的可移植性

Java 的整型常量默认为 int 型,声明 long 型常量后必须加 l 或 L 后缀(建议使用 L 便于辨识)
Java 程序中变量通常声明为 int 型,除非不足以表示较大的数,才使用 long

类型 占用存储空间 表数范围
byte 1字节=8比特 -128 ~ 127 (-2^8 ~ 2^8 - 1)
short 2字节 -2^(2*8-1) ~ 2^(2*8-1) - 1
int 4字节 -2^(4*8-1) ~ 2^(4*8-1) - 1
long 8字节 -2^(8*8-1) ~ 2^(8*8-1) - 1

整型数据类型表数范围为:-2^(占用字节数 * 8 - 1) ~ 2^(占用字节数 * 8 - 1) - 1

数值直接量:为了提高可读性,Java允许在数值直接量的两个数字间使用下划线

long ssn = 123_456_789;

浮点类型

同整型,Java 各浮点类型也有固定的表数范围与字段长度,不受具体操作系统影响
Java 的浮点型常量默认为 double 型,声明 float 型常量必须加后缀 f 或 F

  • float(单精度):尾数可以精确到 7 位有效数字(很多情况下精度很难满足需求)
  • double(双精度):精度是 float 的两倍(通常采用此类型)

浮点型常量两种表示形式:

  • 十进制数形式:3.146.28F.1256(必须有小数点)
  • 科学计数法形式:3.14E26.28e21256E-4
类型 占用存储空间 表数范围
float 4字节 -3.403E38 ~ 3.403E38
double 8字节 -1.798E308 ~ 1.798E308

字符类型(char)

char 型数据用来表示通常意义上的字符(2 字节)
Java 中的所有字符都使用 Unicode 编码,因此一个字符可以存储一个字母、一个汉字或其他书面语的一个字符

字符型变量的三种表现形式:

  • 字符常量是通过单引号(‘’)括起来的单个字符 (char a = ‘a’)
  • 直接使用 Unicode 值来表示字符型常量 (‘\uXXXX’,XXXX 代表一个十六进制整数)、(‘\u000a’ 表示 ‘\n’)
  • Java 中允许使用转义字符 ‘\‘ 来将其后的字符转变为特殊字符型常量
转义字符 含义
‘\b’ (退格符)
‘\n’ (换行符)
‘\r’ (回车符)
‘\t’ (制表符)
‘\‘’ (单引号)
‘\“‘ (双引号)
‘\\‘ (反斜杠)

char 类型是可以进行运算的,因为它都对应有 Unicode 码

ASCII 码

在计算机内部,所有数据都使用二进制表示。每一个二进制位(bit)有 0 和 1 两种状态,因此 8 个二进制位就可以组合出 256 种状态,这被称为一个字节(byte)。一个字节一共可以用来表示 256 种不同的状态,每一个状态对应一个符号,就是 256 个符号,从 00000000 到 11111111

ASCII 码:由美国制定的一套字符编码,对英语字符与二进制位之间的关系做了统一规定。ASCII 码一共规定了 128 个字符的编码

空格(‘SPACE’ 是 32(00100000),’A’ 是 65(01000001),’a’ 是 97)
这 128 个符号(包括 32 个不能打印出来的控制符号),只占用了一个字节的后面 7 位,最前面的 1 位统一规定为 0

布尔型(boolean)

只包含 true 和 false

基本数据类型转换

  • 自动类型转换:容量小的类型自动转换为容量大的数据类型。

  • 强制类型转换:将容量大的数据类型强制转换为容量小的数据类型,可能导致精度降低或溢出

    • 通常字符串不能直接转换为基本类型,但通过基本类型对应的包装类则可以实现把字符串转换成基本类型
    String a = "65";
    int i = Integer.parseInt(a);
    • 强制类型转换格式:强转类型 变量 = (强转类型)原变量;
    long longType = 100L;
    int intType = (int)longType;
  • 有多种类型的数据混合运算时,系统首先自动将所有数据转换成容量最大的那种数据类型,然后再进行计算

  • byte、short、char 之间不会相互转换,它们三者在计算时首先转换为 int 类型(因为三者之间转换很容易超出表数范围)

  • boolean 类型不能与其他数据类型运算,不可以转换为其他数据类型

  • 当把任何基本数据类型的值和字符串(String)进行连接运算时( + ),基本数据类型的值将自动转化为字符串(String)类型

进制

所有数字在计算机底层都以二进制形式存在

对于整数,有四种表示方式:

  • 二进制(binary):0、1,满 2 进 1,以 0b 或 0B 开头表示
  • 十进制(decimal):0-9,满 10 进 1
  • 八进制(octal):0-7,满 8 进 1,以数字 0 开头表示
  • 十六进制(hex):0-9及A-F,满 16 进 1,以 0x 或 0X 开头表示(此处 A-F 不区分大小写)

进制转换:

  • 十进制转其他进制:十进制数除以其他进制取每次余数的倒序表示
  • 其他进制转十进制:右侧第一个数为起始位 0 位,各位上的数字与进制的位次方的乘积求和
  • 其他进制之间进行转换:先转换为十进制再转换为目标进制

二进制:

Java 整数常量默认是 int 类型,当用二进制定义整数时,其第 32 位是符号位。当是 long 类型时,二进制默认占 64 位,其第 64 位是符号位

二进制的整数有三种形式:

  • 原码:直接将一个数值换成二进制数。最高位是符号位
  • 负数的反码:对原码按位取反,最高位(符号位)确定为 1
  • 负数的补码:其反码 +1

计算机以二进制补码的形式保存所有的整数

  • 正数的原码、反码、补码都相同
  • 负数的补码是其反码 +1

运算符

算数运算符:

  • +、-、*、/、%、++、–
  • 加( + )减( - )可作运算、可表正负,表示正负时优先级最高
  • 自增( ++ )自减( – )运算符在前,先运算后取值、自增自减运算符在后,先取值后运算
  • % 取模(求余数)、Java 中只有当被除数为负数时结果才为负

赋值运算符:

  • =、+=、-=、*=、/=、%=
  • 当( = )两侧数据类型不一致时,可以使用自动类型转换或使用强制类型转换原则进行处理
  • 支持连续赋值

比较运算符(关系运算符):

  • ==、!=、<、>、<=、>=、instanceof
  • instanceof 检查是否是类的对象
  • 比较运算符的结果都是 boolean 型,要么是 true 要么是 false

逻辑运算符:

  • &:逻辑与、&&:短路与、|:逻辑或、||:短路或、!:逻辑非、^:逻辑异或

  • 区分 & 与 &&

/*
当 b1、b2 为 true 时打印结果为
1
num1 = 11
1
num2 = 11

当 b1、b2 为 false 时打印结果为
2
num1 = 11
2
num2 = 10
& 为逻辑与、&& 为短路与

*/
boolean b1 = true;
// b1 = false
int num1 = 10;
if (b1 & (num1++ > 0)) {
System.out.println(1);
} else {
System.out.println(2);
}
System.out.println("num1 = " + num1);

boolean b2 = true;
// b2 = false
int num2 = 10;
if (b2 && (num2++ > 0)) {
System.out.println(1);
} else {
System.out.println(2);
}
System.out.println("num2 = " + num2);
  • 区分 | 与 ||
/*
当 b1、b2 为 true 时打印结果为
1
num1 = 11
1
num2 = 10

当 b1、b2 为 false 时打印结果为
1
num1 = 11
1
num2 = 11
| 为逻辑或、|| 为短路或

*/
boolean b1 = true;
// b1 = false
int num1 = 10;
if (b1 | (num1++ > 0)) {
System.out.println(1);
} else {
System.out.println(2);
}
System.out.println("num1 = " + num1);

boolean b2 = true;
// b2 = false
int num2 = 10;
if (b2 || (num2++ > 0)) {
System.out.println(1);
} else {
System.out.println(2);
}
System.out.println("num2 = " + num2);

位运算符:

  • <<:左移(3<<2=12:3*2*2=12)、>>:右移(3>>1=1:3/2=1)、>>>:无符号右移、&:与运算、|:或运算、^:异或运算、~:取反运算
  • <<:在一定范围内,每向左移 1 位,相当于 * 2
  • >>:在一定范围内,每向右移 1 位,相当于 / 2

位运算符的细节:

  • <<:被移除的高位丢弃,空缺位补 0
  • >>:被移位的二进制最高位为 0,右移后,空缺位补 0;最高位为 1,右移后,空缺位补 1
  • >>>:被移位二进制最高位无论是 01,空缺位都用 0
  • &:二进制位进行 & 运算,只有 1 & 1 时结果为 1,否则是 0
  • |:二进制位进行 | 运算,只有 0 | 0 时结果是 0,否则是 1
  • ^:相同的二进制位进行 ^ 运算,结果是 0(1^1=00^0=0)、不相同的二进制位 ^ 运算结果是 1(1^0=10^1=1)
  • ~:正数取反,各二进制码按补码各位取反、负数取反,各二进制位按补码各位取反

位运算是直接对整数的二进制进行的运算

三元运算符:

格式:(条件表达式) ? 表达式1 : 表达式2

  • 条件表达式结果为 true,运算后结果是:表达式1
  • 条件变大时结果为 false,运算后结果是:表达式2
  • 表达式1 和表达式2 是同种类型(整型类型自动类型提升)

三元运算符与 if-else 的联系与区别:

  • 三元运算符可以简化 if-else 语句
  • 三元运算符要求必须返回一个结果
  • if 后的代码块可以有多个语句
  • 三元运算符可以嵌套使用
  • 凡是可以使用三元运算符的地方都可以改写为 if-else 语句,反之不可以

运算符的优先级(运算顺序):只有单目运算符、三元运算符、赋值运算符是从右向左运算的

流程控制

顺序结构

程序从上到下逐行执行,中间没有任何判断和跳转

分支结构

根据条件,选择性地执行某段代码

if…else 分支语句、switch-case 分支语句

if…else 分支语句:

第一种:

if (条件表达式) {
执行表达式
}

第二种(二选一):

if (条件表达式) {
执行表达式1
} else {
执行表达式2
}

第三种(多选一):

if (条件表达式) {
执行表达式1
} else if (条件表达式) {
执行表达式2
}
...
else {
执行表达式 n
}

switch-case 分支语句:

switch(表达式) {
case 常量1:
语句1;
break;
case 常量2:
语句2;
break;
... ...
default:
语句;
break;
}
  • 根据 switch 表达式的值,依次匹配各个 case 中的常量。一旦匹配成功,则进入相应 case 结构中,调用其执行语句
  • 当调用完执行语句后,则仍然继续向下执行其他 case 结构中的执行语句,直到遇到 break 关键字或此 switch-case 结构末尾结束为止
  • break 可以使用在 switch-case 结构中,表示一旦执行到此关键字,就跳出 switch-case 结构
  • switch 结构中的表达式,只能是:byte、short、char、int、枚举类型(JDK5.0新增)、String(JDK7.0新增) 类型之一
  • case 子句中的值只能是常量,不能是变量名或不确定的表达式值,同一个 switch 语句,所有 case 子句中的值不相同
  • break 关键字用来在执行完一个 case 分支后使程序跳出 switch 语句块,如果没有 break,程序会顺序执行到 switch 结尾
  • default 子句是可选的。同时位置也是灵活的,当没有匹配的 case 时,执行 default 子句

if-else 与 switch-case 的关系:

  • 凡是可以使用 switch-case 的结构,都可以转换为 if-else。反之不成立
  • 当分支结构既可以使用 switch-case 又可以使用 if-else (同时,switch 中表达式的取值情况不太多),优先选择 switch-case,因其执行效率稍高

循环结构

根据循环条件,重复性地执行某段代码

while 循环语句、do…while 循环语句、for 循环语句

JDK1.5 后提供了 foreach 循环,方便遍历集合、数组元素

循环语句的四个组成部分:初始化部分(初始化条件)、循环条件部分(循环条件,boolean 类型)、循环体部分(循环体)、迭代部分(迭代条件)

for 循环的结构:

for (初始化条件; 循环条件; 迭代条件) {
循环体
}

while `循环的结构:

初始化条件
while (循环条件) {
循环体;
迭代条件;
}

注意:while 循环没有或错误的迭代条件可能导致死循环

do-while 循环的结构:

初始化条件
do {
循环体;
迭代条件;
} while (循环条件)

while 循环与 do-while 循环的区别在于执行过程,do-while 循环至少会执行一次循环体

最简单的 “无限” 循环格式:while(true)for(;;)

  • 无限循环存在的目的:当程序并不知道需要循环多少次时,需要根据循环体内部某些条件来控制循环的结束

循环结构可以嵌套使用

break 和 continue 关键字的使用:

  • break
    • 使用范围:switch-case、循环结构
    • 作用(不同点):结束当前
  • continue
    • 使用范围:循环结构
    • 作用(不同点):结束当次
  • 相同点:关键字后不能声明执行语句

带标识的 break 和 continue:结束指定标识的一层循环结构

// 求 100000 以内的质数
label:for (int i = 2; i <= 100000; ++i) {
for (int j = 2; j <= Math.sqrt(i); ++j) {
if (i % j == 0) {
continue label;
}
}

System.out.println(i);
}

数组(Array)

多个相同类型数据按一定顺序排列的集合,并使用一个变量命名,通过编号的方式对这些数据进行统一管理

数组的分类

  • 按照维度:一维数组、二维数组、…
  • 按照元素的数据类型:基本数据类型元素的数组、引用数据类型元素的数组(即对象数组)

数组的长度:数组中元素的个数

数组的特点:

  • 数组是有序排列的
  • 数组本身是引用数据类型,而数组中的元素可以是任何数据类型,包括基本数据类型和引用数据类型
  • 创建数组对象会在内存中开辟一整块连续的空间,而数组名中引用的是这块连续空间的首地址
  • 数组的长度一旦确定就不能修改
  • 可以通过下标(或索引)的方式调用指定位置的元素

一维数组的使用

一维数组的声明和初始化:

  • 声明:int[] array;
  • 静态初始化:数组的初始化和数组元素的赋值操作同时进行
array = new int[]{1, 2, 3};
  • 动态初始化:数组的初始化和数组元素的赋值操作分开进行
    • 数组一旦初始化完成,其长度就确定了
    • 类型推断:int[] array = {1, 2, 3, 4};
String[] names = new String[5];

调用数组的指定位置的元素:

  • 使用索引的方式调用
  • 数组的索引从 0 开始,到数组长度 - 1 结束
names[0] = "a";
names[1] = "b";

获取数组的长度:

array.length;
names.length;

遍历数组:

for (int i = 0; i < names.length; ++i) {
System.out.println(names[i]);
}

for (String e: names) {
System.out.println(e);
}

数组元素的默认初始化值:

  • 数组元素是整型:0
  • 数组元素是浮点型:0.0
  • 数组元素是 char 型:0'\u0000' 而非 '0'
  • 数组元素是 boolean 型:false
  • 数组元素是引用数据类型:null

数组的内存解析:

  • 内存的简化结构:

    • 栈(stack):局部变量
    • 堆(heap):new 出来的结构:对象、数组
    • 方法区:常量池、静态域
  • 数组名(变量)在栈空间中,new 出的数组对象(连续空间,以首地址值表示)在堆空间中

  • 默认值替换为初始化值

  • 将首地址值赋给数组名(变量),通过数组名(变量、首地址值)就可以访问堆空间中的数组对象

多维数组

可以把一维数组当成是表格的某一行,那么二维数组就相当于是一整个有多行多列的完整表格

对于二维数组,可以看作是某个一维数组的元素又是一个一维数组,这个元素是一维数组的数组就是二维数组

本质上从数组的底层运行机制来看其实没有多维数组

二维数组的声明和初始化:

  • 静态初始化
int[][] array = new int[][]{{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
  • 动态初始化
String[][] array2 = new String[3][2];
  • 动态初始化
String[][] array3 = new String[3][];
  • 如下也正确
int[] array4[] = new int[][]{{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
int[] array5[] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}; // 类型推断

二维数组的使用:

规定:二维数组分为外层数组的元素,内层数组的元素

int[][] array = new int[4][3];
  • 外层元素:array[0]array[1]
  • 内层元素:array[0][0]array[1][1]

二维数组的默认初始化值:

  • 针对初始化方式一:int[][] array = new int[4][3];

    • 外层元素的初始化值为:(带数据类型的)地址值
    • 内层元素的初始化值为:与一维数组初始化情况一致
  • 针对初始化方式二:int[][] array = new int[4][];

    • 外层元素的初始化值为:null
    • 内层元素的初始化值为:不能调用,否则报错