Java语法
How to write Java programs?
相关信息
First,了解基本语法!
关键字
Java 中有 51 个关键字和 2 个保留字,关键字不能用于命名常量、变量、和任何标识符。
类别 | 关键字 | 说明 |
---|---|---|
访问控制 | private | 私有的 |
访问控制 | protected | 受保护的 |
访问控制 | public | 公共的 |
类、方法和变量修饰符 | abstract | 声明抽象 |
类、方法和变量修饰符 | class | 类 |
类、方法和变量修饰符 | extends | 扩充,继承 |
类、方法和变量修饰符 | final | 最终值,不可改变的 |
类、方法和变量修饰符 | implements | 实现(接口) |
类、方法和变量修饰符 | interface | 接口 |
类、方法和变量修饰符 | native | 本地,原生方法(非Java实现) |
类、方法和变量修饰符 | new | 新,创建 |
类、方法和变量修饰符 | static | 静态 |
类、方法和变量修饰符 | strictfp | 严格,精准 |
类、方法和变量修饰符 | synchronized | 线程,同步 |
类、方法和变量修饰符 | transient | 短暂 |
类、方法和变量修饰符 | volatile | 易失 |
程序控制语句 | break | 跳出循环 |
程序控制语句 | case | 定义一个值以供switch选择 |
程序控制语句 | continue | 继续 |
程序控制语句 | default | 默认 |
程序控制语句 | do | 运行 |
程序控制语句 | else | 否则 |
程序控制语句 | for | 循环 |
程序控制语句 | if | 如果 |
程序控制语句 | instanceof | 实例 |
程序控制语句 | return | 返回 |
程序控制语句 | switch | 根据值选择执行 |
程序控制语句 | while | 循环 |
错误处理 | assert | 断言表达式是否为真 |
错误处理 | catch | 捕捉异常 |
错误处理 | finally | 有没有异常都执行 |
错误处理 | throw | 抛出一个异常对象 |
错误处理 | throws | 声明一个异常可能被抛出 |
错误处理 | try | 捕获异常 |
包相关 | import | 引入 |
包相关 | package | 包 |
基本类型 | boolean | 布尔型 |
基本类型 | byte | 字节型 |
基本类型 | char | 字符型 |
基本类型 | double | 双精度浮点 |
基本类型 | float | 单精度浮点 |
基本类型 | int | 整型 |
基本类型 | long | 长整型 |
基本类型 | short | 短整型 |
基本类型 | null | 空 |
变量引用 | super | 父类,超类 |
变量引用 | this | 本类 |
变量引用 | void | 无返回值 |
保留关键字 | goto | 是关键字,但不能使用 |
保留关键字 | const | 是关键字,但不能使用 |
注释
代码注释是程序代码可维护性的重要环节之一。 编译器会忽略这些代码,不会出现在可执行程序中。
在 Java 中有以下三种注释方式:
int i = 0; // 这里是单行注释,在双下划线后
// TODO 特殊标记, 备注此处未完成
// XXX 特殊标记, 此处代码有待商榷、改进
// FIXME 特殊标记, 此处代码待修复
/*
这里是多行注释,
以下划线+星号开始
以星号+下划线结束
*/
int i = 0;
/**
* 这里是文档注释
* 是多行的
* 以下划线+两个星号开始
* 以星号+下划线结束
* 可以使用文档注释标记
* @author LingYuan 标记类,表示作者名
* @version 1.0 标记类或方法,表示版本号
* */
public class HelloWorld() {
/**
* @param msg 标记方法,表示入参释义
* @return 标记方法,表示返回值释义
* @exception Exception 标记方法,表示方法抛出的异常
* @see 标记类或方法,表示参考转向
*/
public void print(String msg) throws Exception {
// doSomething
}
}
注释规范
- 必须加的基本注释:类,接口,构造函数,方法,全局变量,属性。
- 必须加的特殊注释:算法,代码不明晰,代码修改处,循环与选择分支嵌套,向外提供的接口。
- 使用统一的标点符号。
- 内容简单明了、含义准确、内容不大于10个字。
get/set
方法不需加注释。
基本数据类型
Java 中有 8 个基本数据类型,分为整型、浮点型、字符型、布尔型。
整型
4 个整型,用于表示没有小数部分的数值,可以是负数。
存储范围:2 M-1 ,M为存储空间位数,首位为符号位,故 - 1.
类型 | 大小(字节) | 取值范围 |
---|---|---|
long | 8 | -263 ~ 263 - 1 |
int | 4 | -231 ~ 231 - 1 |
short | 2 | -215 ~ 215 - 1 |
byte | 1 | -27 ~ 27 - 1 |
位与字节(扩展)
位(bit):数据存储最小单位,存储一个二进制位 0 或 1.
字节(byte):1byte = 8bit,1KB = 1024 B(字节),1MB = 1024KB,1GB = 1024MB,以此类推。
提示
以 int 为例,其使用 4 个字节,即 4 * 8 = 32 位。每一位存储一个二进制位,那么其取值范围应该为 2^32 - 1.
但 int 可以是负数,所以存储空间中首位是符号位用来区分正负值,故其可以表示的值范围为 -2^31 - 1 ~ 2^31 - 1. 但...这很明显还不对。
在 Java 中负数的取值是由正数取反加1的规则,既取补码得到,目的是为了正负数在计算时可以使用同一套逻辑。 故:
int 类型的最大值为:0111 1111 1111 1111 1111 1111 1111 1111 = 2^31 - 1
int 类型的最小值为:1000 0000 0000 0000 0000 0000 0000 0000 ,其值本应当为 0,但数值 0 已经由 0000 0000 0000 0000 0000 0000 0000 0000
表示。
故将1000 0000 0000 0000 0000 0000 0000 0000
的数值定义为 -2^31,故 int 类型可以表示的值范围为 -2^31 ~ 2^31 - 1.
浮点型
Java 中有 2 个浮点类型,用于表示有小数部分的数值。
存储范围:M * 2E,M是尾数,E是指数。指数E中一位为符号位,一位用于指数的偏移量。故有效数字为E - 1 或E - 2.
类型 | 大小(字节) | 取值范围 |
---|---|---|
double | 8 | M占46位,E占17位,符号位1位(有效位数为15~16位) |
float | 4 | M占23位,E占8位,符号位1位(有效位数为6~7位) |
- float类型的数值需有后缀F或者f,没有后缀F的浮点数值默认转换为double类型。
- double类型的数值需有后缀D或者d,可以省略。
注意
由于浮点数是近似表示的,在进行浮点数运算时,可能会出现舍入误差。如果在数值计算中不允许有任何舍入误差,就应该使用 BigDecimal 类。
这种误差是由于浮点数值采用二进制表示,二进制无法精准表示分数 1/10
,就好像十进制中无法精准地表示分数 1/3
一样。
**浮点类型应避免进行比较。**表示溢出和出错情况的特殊的浮点数值:正无穷大(一个正整数除以0)、负无穷大、NaN(计算 0/0
或者负数的平方根)。
字符类型
字符类型 char 表示单个字符。
char 类型的字面值使用单引号包括(如 'A' 是编码值为 65 的字符常量,这与 "A" 不同,"A"是包含一个字符 A 的字符串)。
Unicode
Unicode 编码规则打破了传统字符编码机制的限制。在Unicode出现前,有许多不同的标准(美国的ASCII,西欧的ISO8859-1,俄罗斯的KOI-8,中国的GB-18030和BIG-5等),这导致在不同的编码方案下可能对应不同的字母。
Unicode 启动了统一工作。 在 Java中,char 类型描述了 UTF-16 编码中的一个代码单元。强烈建议不要在程序中使用 char 类型,除非确实需要处理 UTF-16 代码单元,最好将字符串作为抽象数据类型处理。
Unicode 字符编码需要用一个或两个 char 值描述。char 类型的值可以表示为十六进制值,范围从 \u0000
~ \uFFFF
,\u2122
表示商标符号(TM),\u03C0
表示希腊字母(Π)。
提示
字符:a、A、汉、+、-、$…均表示一个字符,在不同编码下,汉字字符占用字节不同,UTF8(3个字节),GBK(2个字节)。
字符集:收录标准的各个字符的集合。
编码: 规定字符存储规定,实际是对字符集中的字符进行编码,用二进制格式存储,展示时再用对应的编码解析(解码),常见的编码规则:gbk、Unicode、ASCii…
布尔类型
布尔类型 boolean 仅有两个常量值:true、false,用来表示逻辑真或假。
类型转换
将一种数据类型转换为另一种数据类型。低精度到高精度数据类型可以自动转换。高精度到低精度需要强制转换,会丢失部分精度。布尔类型不能进行转换。
数据类型的精度从低到高为:byte,short,char,int,long,float,double.
强制转换语法:类型 变量名
提示
- 用二元运算符计算两个值时,会将两个操作数转换为同一种类型。
- 将浮点数转换为整型时,精度丢失方式是通过截断小数的部分。
- 类型转换时,如果超出了目标类型的表示范围,结果就会截断成一个完全不同的值。
(byte) 300
实际值为 44.
进制标记
从 JDK7 开始,可以加上前缀标记不同的进制。
0b
或 0B
标记二进制数(如 0b1001
对应十进制中的 9)。
0x
或 0X
标记十六进制数(如 0xCAFE
)。
0
标记八进制数(如 010
对应十进制中的 8),八进制显然很容易混淆,尽量不要使用八进制常数。
包装类
包装类是基于基本数据类型的封装,值允许为 NULL 且默认为 NULL,并提供了实用 API.
基本数据类型 | 包装类 |
---|---|
long | Long |
int | Integer |
short | Short |
byte | Byte |
double | Double |
float | Float |
boolean | Boolean |
char | Character |
基本数据类型与对应的包装类型互相转换的过程称为装拆箱,装拆箱是自动进行的,应避免非必要的装拆箱,否则将影响系统性能。
Integer i = 40; // 装箱 等价于 Integer i = Integer.valueOf(40);
int j = i; // 拆箱 等价于 int j = i.intValue();
提示
包装类属于对象类型,对象的值之间进行比较必须使用 equals
方法。
常量池
大部分包装类默认创建了部分常用数值,使用以下范围的数值将直接指向常量值中的数值:
- 整型包装类缓存值(-127 ~ 128)。
Character
缓存值(0 ~ 127)。Boolean
缓存值(True 、False)。- 浮点型包装类没有使用常量池技术。
注意
需注意当使用常量池缓存时,值相等的两个对象使用 ==
进行比较也能返回 true
,因为对象的引用都指向常量池中的数值。
常量与变量
变量是指一个抽象的储存地址,地址中存储实际数据,变量名则是引用数据用的地址别名。 变量的声明可以放在任何地方,但要尽可能靠近首次使用的地方,保持良好的编码风格。 声明变量后,必须对变量赋值将其显式初始化,不建议使用未初始化的变量。。
Java 是强类型语言,变量在定义时必须指定其数据类型(基本数据类型、包装类、对象类型)。
JDK10 开始,对于局部变量,如果可以从初始值推断出类型,就不再需要声明类型,可以使用关键字 var
而无须指定类型。
变量命名规则:以字母、数字、下划线、美元符号组成,不能以数字开头,不能与关键字同名,大小写敏感,长度基本没有限制。
常量是不可变的变量,在声明时就必须赋值,赋值后不可更改,使用 final
关键字修饰一个变量即为声明常量。命名规范:字母全大写,单词以下划线隔开。
运算符
Java 提供了一组丰富的算数和逻辑运算符以及数学函数。使用括号可以控制运算符优先级,括号中的运算最优先。不使用括号时就按照运算符优先级次序进行计算。
算数运算
运算符 | 意义 |
---|---|
+ | 加法 |
- | 减法 |
* | 乘法 |
/ | 除法 |
% | 取模,左操作数除以右操作数的余数 |
运算规则:
- 当 0 作为被除数时,除数如果为整数将会产生一个异常,除数为浮点数时将会得到无穷大或 NaN 结果。
- 结合赋值方式:
x += 4;
等价于x = x + 4;
.
关系运算
关系运算符全部返回 boolean 类型
运算符 | 意义 |
---|---|
== | 检测相等性 |
!= | 检测不相等 |
> | 大于 |
< | 小于 |
>= | 大于等于 |
<= | 小于等于 |
逻辑运算
逻辑运算参数全都为 boolean 类型,返回也都为 boolean 类型。
运算符 | 意义 |
---|---|
&& | 逻辑与,当两个参数都为真,条件为真。 |
|| | 逻辑或,当任一参数为真,条件为真。 |
! | 逻辑非,反转参数的逻辑状态。 |
运算规则:
&&
和||
运算按照短路方式求值:如果第一个操作数已经能够确定表达式的值,则不计算第二个操作数,可以避免 NPE.
位运算
运算符 | 意义 |
---|---|
& | 与。如果相对应位都是1,则结果为1,否则为0 |
| | 或。如果相对应位都是 0,则结果为 0,否则为1 |
^ | 异或。如果相对应位值相同,则结果为0,否则为1 |
~ | 取反。翻转操作数的每一位,即0变成1,1变成0 |
<< | 左移。二进制各位全部左移若干位,高位丢弃,低位补0 |
>> | 右移。二进制各位全部右移若干位,高位补0 |
>>> | 按位右移补零操作符。左操作数的值按右操作数指定的位数右移,移动得到的空位以0填充。 |
运算规则:
- 不存在
<<<
运算符。 - 计算机中进行
+-*/
运算都是转换为二进制运算,合理的运用位运算可以加快程序运行速度。 - 对一个数使用
<<1
,相当于操作数*2
. - 对一个数使用
>>1
,相当于操作数/2
. - 对一个数进行
~
运算,再进行&
运算,可以找到二进制数中最右的数值为 1 的位置。
自增自减
使变量加一或减一,前缀形式会先完成加1,而后缀形式会优先使用变量原先的值。
- 不建议在表达式中使用自增自减运算符,这样的代码容易造成困惑。
运算符 | 意义 |
---|---|
++ | 自增,操作数加一 |
-- | 自减,操作数减一 |
三元运算符
运算符 | 意义 |
---|---|
boolean ? value1 : value2 | ? 前应为一个 boolean 表达式,为 true 时返回 : 前的值,否则返回其后的值。 |
流程控制
程序运行有三种流程结构:顺序(默认,从上往下执行)、选择、循环。
选择结构
选择结构指在满足(或不满足)某个条件时,执行(或不执行)某段代码。
boolean flag = false;
if (flag) {
// 如果 flag 为真则执行此块
// 可以单独使用,也可以搭配 else if 和 else 使用
// doSomething
} else if (1 == 2) {
// 当以上条件都不满足时,且当前表达式为真时执行此块
// 必须跟在 if 后,且可有多个else if
// doSomething
} else {
// 当以上条件都不满足时执行此块
// 必须跟在 if 或 else if 后,只能有一个
// doSomething
}
int age = 18;
switch (age) {
case 16 :
// 当 age = 16 时执行此块
// 如果不使用 break 或 continue 则会继续执行下去
// 不要忘记使用 break,否则最终可能进入多个 case 块或 default 块
case 17 :
// 当age为17时执行此块
case 18 :
// 当age为17时执行此块
default :
// 当没有被break或没有满足任何条件时 到达判断时默认会执行此块
// default 块是可选的,而编码规范要求必须有
}
循环结构
循环结构指满足条件时,反复执行某段代码的行为。
// 在已知循环次数时使用
// 三段表达式作用分别是:初始化变量; 布尔表达式; 更新变量
for(int i = 0; i < 9; i++){
// 在满足布尔表达式时执行此块
}
// 常用在不能明确循环次数时
// 需要在循环中转换布尔表达式的值,使跳出循环
// 前置用法
while(true){
// 当布尔表达式为真时执行此块
}
// 后置用法,先执行一次再执行判断
do {
// 循环块
} while(true);
// Iterator 是集合接口的对象,可以用于实现循环结构。
// 可以在循环集合的同时删除元素,避免常规循环结构中删除元素造成的 NPE.
String[] arr = new String[] {"a", "b", "c", "d"};
List<String> list = Arrays.stream(arr).collect(Collectors.toList());
// 获取集合的迭代器
Iterator<String> it = list.iterator();
while (it.hasNext()) {
System.out.println(it.next());
// 删除元素
it.remove();
}
// 此时list已经为空
list.forEach(System.out::println);
// 增强 for 循环,迭代器的语法糖
String[] arr = new String[] {"a", "b", "c", "d"};
for (String cur : arr) {
// 遍历数组每个元素执行此块
}
// 集合中的forEach方法
List<String> list = Arrays.stream(arr).collect(Collectors.toList());
list.forEach(System.out::println);
break continue
break:跳出当前循环块或 switch 块。
continue:结束当次循环,继续下一轮循环。
类
类是组织代码的基本单元,每个 .java
文件只允许有一个公共类,且要求与文件同名。
// 定义一个公共类
public class Dog {
}
类修饰符
- 类可以被
final
修饰,表示类为最终的,不可被继承。 - 类可以被
static
修饰,但只能修饰内部类。被 static 修饰的内部类可以直接作为一个普通类来使用,而不需实例一个外部类。
继承
使用 extends
关键字使一个类(子类)继承一个类(父类),子类继承父类非私有属性和方法,无法继承父类的构造方法。使子类能扩展新的功能,通过继承来删减同一类事物的相同代码。
在 Java 中只允许单继承,一个类只能有一个直接父类,所有的类(除了基本数据类型)全部直接或间接的继承 Object
类,
在创建子类实例时需先加载父类再加载子类,所以在子类的构造方法中会默认调用父类的无参构造方法。
如果子类重写了父类中的方法,不管怎么调用全部执行的是子类重写后的方法。
在子类的方法中通过 super 调用父类的属性以及方法,先执行父类的静态方法-->子类的静态方法-->父类的非静态方法-->子类的非静态方法。
super
:在子类中代表父类对象,可以通过 super 调用父类的方法和属性。this
:表示本类对象,在构造方法中区分局部变量和全局变量,在方法中调用本类的另一个方法。instanceof
:判断两个对象有无父子关系。
抽象类
使用 abstract 修饰的类,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就可以设计为抽象类。抽象类不能创建实例,唯一的作用是为了让子类继承,并且子类必须实现父类的抽象方法。
抽象类中可以定义抽象方法,抽象方法没有方法体,子类必须重写。抽象类不一定有抽象方法,但有抽象方法必须是抽象类。
接口
使用 interface 代替 class 声明一个接口,接口是一系列方法头的声明,不能用来创建实例,接口中的属性全部是常量,方法全部是抽象方法。
类使用 implement 实现接口,可以实现多个接口,接口可以继承多个接口,子类必须重写接口中的所有方法。
静态
静态是在编译后所分配的内存会一直存在,直到程序退出内存才会释放这个空间。静态区和程序一起加载,在类加载时就执行,不需要创建实例。
用 static 修饰的成员变量和成员方法可以通过类名加 . 进行直接访问,不需要实例化就可以使用,是所有该类实例共享的属性,无论创建多少个实例,静态成员只会执行一次,因为类只加载一次。 静态方法中在实例化前便加载了,此时无法调用普通方法,也无法使用 this 和 super 关键字。
在类中可以使用 static
定义静态静态代码块,JVM 加载类时会执行这些静态的代码块,如果代码块有多个,将按照先后顺序依次执行,每个代码块只会被执行一次。
public class Animal {
static {
// 静态代码块,随着类加载一起执行
// 用于给静态变量赋初始值?
// doSomething
}
}
访问修饰符
访问修饰符用于修饰方法、类、全局变量,通常在语句最前端。
Java 中包括 4 个访问修饰符(范围从大到小):
- public:可在任何地方被调用。
- protected:可在本包及子类中被调用。
- default:可在本包中被调用。
- private:仅可在本类中被调用。
方法
方法是程序语句的集合,在 C++ 中称为函数。它是解决一类问题的代码块,定义在类中。
定义方法
public class Animal {
/**
* 定义/声明一个方法
* public - 访问修饰符
* int - 返回值类型(基本数据类型,对象类型,void)
* eat - 方法名
* String food, int number - 形式参数类型/参数名
* throws RuntimeException - 声明方法可能抛出的异常,调用此方法需处理
*/
public int eat(String food, int number) throws RuntimeException, NullPointerException {
// 方法体
System.out.println("eat" + food + "*" + number);
return 1;
}
}
调用方法
在同一个类中使用 this.方法名(实参)
调用,this.
可以省略:
public class Animal {
public int eat(String food, int number) throws RuntimeException, NullPointerException {
// 方法体
System.out.println("eat" + food + "*" + number);
return 1;
}
/**
* 调用其他方法的方法
*/
public void doInvoke() {
this.eat("apple", 1);
}
}
在子类中使用 super.方法名(实参)
调用:
/**
* 声明一个 Animal 的子类
* 子类默认拥有父类非 private 的属性和方法
*/
public class Dog extends Animal {
public void doInvoke() {
super.eat("apple", 1);
}
}
在其他类中调用,需要创建方法的对象的实例,在满足访问范围的条件下,通过实例调用方法:
public class Cat {
public void doInvoke() {
// 使用 new 关键字实例化一个 Dog 类
Dog dog = new Dog();
dog.eat("apple", 1);
}
}
静态方法
静态方法是使用 static
修饰的方法,会在类定义的时候被装载到内存中。可以直接通过类调用静态方法,不需要创建一个类的实例。
在静态方法中不能调用非静态方法,因为静态区会优先加载,此时非静态方法还未加载。
public class Animal {
public int eat(String food, int number) throws RuntimeException, NullPointerException {
System.out.println("eat" + food + "*" + number);
return 1;
}
public static void drink() {
System.out.println("drink water");
// 此方法体内无法调用 eat()
}
}
主方法
主方法是特殊的方法,所有的 Java 程序由 public static void main(String[] args)
方法开始执行。
public class FirstCode {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
构造方法
每个类都有的特殊方法,用于创建当前类的实例对象。在不显式声明时,默认隐式拥有一个无参构造方法。
每个实例的创建都要经过构造方法,可能利用这个特性初始化数据。
public class Animal {
private int age;
/**
* 定义构造方法,方法名与类名一致,无返回值参数
* 无参构造方法,未定义有参构造方法时,自动隐式定义
*/
public Animal () {
System.out.println("Animal 类被实例化");
}
/**
* 定义有参构造方法
*/
public Animal(int age) {
// this 表示代表本类 Animal
this.age = age;
}
}
方法重写
方法重写是指子类继承父类后,对父类方法进行重写,是实现多态的一种方式。
重写方法的规则:
- 方法名、形式参数列表必须相同。
- 返回类型可以不相同,但是必须是父类返回值的派生类( java5 及更早版本返回类型要一样,java7 及更高版本可以不同)。
- 修饰符范围可以扩大但不能缩小。
- 抛出的异常范围可以缩小但不能扩大。
- 重写的方法使用
@Override
注解标记。 - 声明为
final
的方法不能被重写。 - 声明为
static
的方法不能被重写,但是能够被再次声明。 - 重写方法的范围受到访问修饰符的控制,子类要能访问到父类的方法。
public class Animal {
public void say() {
System.out.println("---");
}
}
public class Dog extends Animal {
@Override
public void say() {
System.out.println("汪汪汪");
}
}
方法重载
方法重载是指在一个类中,方法名称相同,但形参不同的函数,是实现多态的一种方式。编译器会根据调用方法的参数个数、类型等逐个匹配,选择对应的方法执行。
重载方法的规则:
- 方法名称相同。
- 形参列表不同(个数不同,数据类型不同,顺序不同)。
- 返回值可以相同或不同(如果仅返回值不同,无法实现方法重载)。
public class Animal {
public void eat(String food) {
System.out.println("eat" + food);
return 1;
}
public int eat(String food, int number) {
System.out.println("eat" + food + "*" + number);
return number;
}
}
可变参数
从 JDK1.5 开始,Java 支持传递同类型的可变参数给一个方法,在不能确认传递给方法的实际参数的个数时使用。
声明方法:
- 一个方法中只能指定一个可变参数,它必须是方法的最后一个参数,任何普通的参数必须在它之前声明。
- 在方法声明中,参数类型后加一个省略号
...
. - 可变参数在方法中是一个数组类型。
public class Calculator {
/**
* 打印最大的数
* */
public static void printMax(Double... numbers) {
if (numbers.length == 0) {
System.out.println("No argument passed");
return;
}
double result = numbers[0];
for (int i = 1; i < numbers.length; i++) {
result = result > numbers[i] ? result : numbers[i];
}
System.out.println("Max number is " + result);
}
public static void main(String[] args) {
printMax(2.0, 3.9, 1.1, 0.99);
}
}
递归
递归是方法在方法体中调用自己的行为。通过递归可以找到局部最优解,可以用简单的方法解决一些复杂的问题。
需要注意方法的调用是一种压栈的过程,当不确认方法的调用次数是否可控时,不建议使用递归,可能导致栈溢出。
public class Calculator {
/**
* 计算阶乘
*/
public static long getFactorial(int number) {
if (number == 1) {
return 1;
}
return number * getFactorial(number - 1);
}
public static void main(String[] args) {
System.out.println(getFactorial(5));
}
}
枚举
枚举是一个特殊的类,一般表示一组常量,当可以列出某些有穷序列集的所有成员时,可以定义枚举类标记。
使用关键字 enum
替代 class
定义一个枚举类,其中每个枚举值都是 public static final
的。
所有的枚举都继承自 java.lang.Enum
类。由于Java 不支持多继承,所以枚举对象不能再继承其他类。
public enum ColorEnum {
RED("red", "红色"),
BLUE("blue", "蓝色"),
GREEN("green", "绿色"),
;
// 成员变量
private String code;
private String desc;
// get/set方法
public String getCode() {return code;}
public void setCode(String code) {this.code = code;}
public String getDesc() {return desc;}
public void setDesc(String desc) {this.desc = desc;}
// 普通方法
public static String getDesc(String code) {
if (Objects.isNull(code) || code.isEmpty()) {
return null;
}
for (ColorEnum ele : ColorEnum.values()) {
if (ele.code.equals(code)) {
return ele.getDesc();
}
}
return null;
}
// 构造方法
ColorEnum(String code, String desc) {
this.code = code;
this.desc = desc;
}
}
内部类
Java 允许在类中再定义类,称为内部类。
内部类分为以下四种:
- 成员内部类
- 静态内部类
- 局部内部类
- 匿名内部类
成员内部类
成员内部类是外部类的一个成员,拥有访问修饰符。实例化成员内部类的前提是必须实例化一个外部类。
成员内部类可以无条件访问外部类的所有成员属性和成员方法(包括 private 和静态成员)。当出现和外部类同名的成员变量或者方法时,默认访问的是成员内部类的成员。 要访问外部类的同名成员,需要以下面的形式进行访问:外部类.this.成员变量/方法
。
外部类也可以访问内部类的所有成员变量和方法(包括 private),但必须先创建一个成员内部类的对象,再通过这个对象的引用来访问。
相关信息
外部类变量是怎么传递给内部类的?
- 非
final
局部变量,通过构造器的方式传递进来的。 final
局部变量,如果在编译期间是确定的,编译器聪明的将表达式直接优化为常量数值。但如果常量赋值是运行时才能确定的,那么还是采用构造器传参的形式实现。- private 变量,会生成
get/set
方法。
因为非静态成员内部类要依赖外部类,所以成员内部类中不能存在任何 static
的变量和方法(除非是使用 final static
同时修饰且属性字段是基本类型或者 String
类型的,那么是可以编译通过的,因为对于 final static
的变量是存放在常量池中的,不涉及到类的加载)。
应用场景:
- 使用成员内部类来定义复杂结构的api接口响应数据。
- 使用成员内部类变相的实现类的多继承。
- 实现单例模式。
定义复杂结构的接口响应数据
public class Response {
private Integer code;
private String message;
private List<Data> datas;
/**
* 成员内部类
*/
public class Data {
public Long id;
public String name;
// 存储至常量池,不影响类的加载
final static String version = "1";
}
/**
* 提供实例化内部类的方法
*/
public Data getData() {
return new Data();
}
public static void main(String[] args) {
Response resp = new Response();
// 创建内部类的两种方式
Data data1 = resp.getData();
Data data2 = resp.new Data();
}
}
静态内部类
静态内部类是静态的成员内部类。只有内部类能使用 static
修饰,
非静态内部类在编译完成之后会隐含地保存着一个引用指向创建它的外部类的对象。但静态内部类没有,没有这个引用就意味着:
- 不需要依赖于外部类的对象就可创建。
- 不能使用外部类的非
static
成员变量和方法。 - 允许有
static
属性、方法。
局部内部类
局部内部类是定义在方法或者作用域内的类,访问仅限于方法内或者该作用域内。不能有访问修饰符以及 static
修饰符。
public class FirstCode {
public void print() {
/**
* 局部内部类
*/
class Person {
private String name;
public Person(String name) {
super();
this.name = name;
}
public void say(String msg) {
System.out.println(name + ":" + msg);
}
}
new Person("LingYuan").say("Hello World!");
}
}
匿名内部类
匿名内部类是唯一一种没有构造器的类(因为类名编译完成后才确定,不能自定义构造器)。在编译时由系统自动起名为 Outter$[内部类名].class
,一般用于继承类或实现接口时对继承方法的重写或实现。
大部分匿名内部类用于接口回调,Lambda 表达式可以很方便返回一个匿名内部类,但并不是匿名内部类的语法糖,而是基于 invokedynamic
指令,在运行时使用ASM生成类文件来实现。
相关信息
匿名内部类使用外部类方法中的局部变量为何需要是 final 类型的?
由于匿名内部类传递变量的实现是基于构造器传参,所以不能在匿名内部类中修改外部局部变量,如果允许在匿名内部类中修改值,修改的是匿名内部类中的外部局部变量副本,最终并不会对外部类产生效果,这样可能引起困扰,所以就禁止在匿名内部类中修改外部局部变量。
匿名内部类不是一定需要局部变量是 final
的,使用外部类方法中的非 final
修饰的局部变量,如果在内部类中没有修改的局部变量的值,编译运行没问题,因为编译器很智能,由于没有修改值,所以编译器认为这是 effectively final.
public class FirstConcurrentCode {
/**
* 实例化一个 Runnable 类型,需实现其 run 方法
* 直接使用匿名内部类来实现它
*/
private Runnable runnable = new Runnable() {
// 匿名内部类
@override
public void run() {}
}
}
包管理
Java 包机制,用于区别类名的命名空间。如果要在某个类中使用其他类成员,需要在当前类中导入那个类。
package [url]
:声明当前类所在的包。
import [url]
:导入包或对象。
值传递 引用传递
值传递(pass by value):在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
引用传递(pass by reference):在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。
在 Java 中参数传递方式是 值传递,但是对于对象参数,值的内容是对象的引用,即等同于引用传递。