基础知识
整型
https://www.bookstack.cn/read/liaoxuefeng-java/818efb1cf2ca559b.md
public class HelloWorld {
public static void main(String []args) {
int i3 = 2_000_000_000;
System.out.println(i3);
// 十六进制
int i4 = 0xf;
System.out.println(i4);
// 二进制
int i5 = 0b1111;
System.out.println(i5);
// long
long i6 = 9000000000000000000L;
System.out.println(i5==i4);// true
}
}
浮点型
float f1 = 3.14f;
float f2 = 3.14e38f; // 科学计数法表示的3.14x10^38
double d = 1.79e308;
double d2 = -1.79e308;
double d3 = 4.9e-324; // 科学计数法表示的4.9x10^-324
布尔类型
boolean b1 = true;
boolean b2 = false;
boolean isGreater = 5 > 3; // 计算结果为true
int age = 12;
boolean isAdult = age >= 18; // 计算结果为false
字符类型
字符类型char表示一个字符。Java的char类型除了可表示标准的ASCII外,还可以表示一个Unicode字符:
public class HelloWorld {
public static void main(String []args) {
char a = 'A';
char zh = '中';
System.out.println(a);
System.out.println(zh);
}
}
注意char类型使用单引号',且仅有一个字符,要和双引号"的字符串类型区分开。
常量
定义变量的时候,如果加上final修饰符,这个变量就变成了常量:
final double PI = 3.14; // PI是一个常量
double r = 5.0;
double area = PI * r * r;
PI = 300; // compile error!
常量在定义时进行初始化后就不可再次赋值,再次赋值会导致编译错误。
根据习惯,常量名通常全部大写。
var关键字
有些时候,类型的名字太长,写起来比较麻烦。例如:
StringBuilder sb = new StringBuilder();
这个时候,如果想省略变量类型,可以使用var关键字:
var sb = new StringBuilder();
编译器会根据赋值语句自动推断出变量sb的类型是StringBuilder。对编译器来说,语句:
var sb = new StringBuilder();
实际上会自动变成:
StringBuilder sb = new StringBuilder();
因此,使用var定义变量,仅仅是少写了变量类型而已。
变量的作用范围
只要正确地嵌套这些{ },编译器就能识别出语句块的开始和结束。而在语句块中定义的变量,它有一个作用域,就是从定义处开始,到语句块结束。超出了作用域引用这些变量,编译器会报错。举个例子:
{
...
int i = 0; // 变量i从这里开始定义
...
{
...
int x = 1; // 变量x从这里开始定义
...
{
...
String s = "hello"; // 变量s从这里开始定义
...
} // 变量s作用域到此结束
...
// 注意,这是一个新的变量s,它和上面的变量同名,
// 但是因为作用域不同,它们是两个不同的变量:
String s = "hi";
...
} // 变量x和s作用域到此结束
...
} // 变量i作用域到此结束
定义变量时,要遵循作用域最小化原则,尽量将变量定义在尽可能小的作用域,并且,不要重复使用变量名。
移位运算
在计算机中,整数总是以二进制的形式表示。例如,int
类型的整数7
使用4字节表示的二进制如下:
00000000 0000000 0000000 00000111
可以对整数进行移位运算。对整数7
左移1位将得到整数14
,左移两位将得到整数28
:
int n = 7; // 00000000 00000000 00000000 00000111 = 7
int a = n << 1; // 00000000 00000000 00000000 00001110 = 14
int b = n << 2; // 00000000 00000000 00000000 00011100 = 28
int c = n << 28; // 01110000 00000000 00000000 00000000 = 1879048192
int d = n << 29; // 11100000 00000000 00000000 00000000 = -536870912
int n = 7; // 00000000 00000000 00000000 00000111 = 7
int a = n >> 1; // 00000000 00000000 00000000 00000011 = 3
int b = n >> 2; // 00000000 00000000 00000000 00000001 = 1
int c = n >> 3; // 00000000 00000000 00000000 00000000 = 0
如果对一个负数进行右移,最高位的1
不动,结果仍然是一个负数:
int n = -536870912;
int a = n >> 1; // 11110000 00000000 00000000 00000000 = -268435456
int b = n >> 2; // 10111000 00000000 00000000 00000000 = -134217728
int c = n >> 28; // 11111111 11111111 11111111 11111110 = -2
int d = n >> 29; // 11111111 11111111 11111111 11111111 = -1
还有一种不带符号的右移运算,使用>>>
,它的特点是符号位跟着动,因此,对一个负数进行>>>
右移,它会变成正数,原因是最高位的1
变成了0
:
int n = -536870912;
int a = n >>> 1; // 01110000 00000000 00000000 00000000 = 1879048192
int b = n >>> 2; // 00111000 00000000 00000000 00000000 = 939524096
int c = n >>> 29; // 00000000 00000000 00000000 00000111 = 7
int d = n >>> 31; // 00000000 00000000 00000000 00000001 = 1
对byte
和short
类型进行移位时,会首先转换为int
再进行位移。
仔细观察可发现,左移实际上就是不断地×2,右移实际上就是不断地÷2。
类型自动提升与强制转型
在运算过程中,如果参与运算的两个数类型不一致,那么计算结果为较大类型的整型。例如,short
和int
计算,结果总是int
,原因是short
首先自动被转型为int
:
short s = 1234;
int i = 123456;
int x = s + i; // s自动转型为Int
short y = s + i;//编译错误
也可以将结果强制转型,即将大范围的整数转型为小范围的整数。强制转型使用(类型)
,例如,将int
强制转型为short
:
int i = 12345;
short s = (short) i; // 12345
要注意,超出范围的强制转型会得到错误的结果,原因是转型时,int
的两个高位字节直接被扔掉,仅保留了低位的两个字节:
public class HelloWorld {
public static void main(String []args) {
int i1 = 1234567;
short s1 = (short) i1; // -10617
System.out.println(s1);
int i2 = 12345678;
short s2 = (short) i2;
System.out.println(s2); // 24910
}
}
因此,强制转型的结果很可能是错的。
小结
整数运算的结果永远是精确的;
运算结果会自动提升;
可以强制转型,但超出范围的强制转型会得到错误的结果;
应该选择合适范围的整型(int
或long
),没有必要为了节省内存而使用byte
和short
进行整数运算。
整数运算
整数的数值表示不但是精确的,而且整数运算永远是精确的,即使是除法也是精确的,因为两个整数相除只能得到结果的整数部分:
int x = 12345 / 67; // 184
求余运算使用%
:
int y = 12345 % 67; // 12345÷67的余数是17
特别注意:整数的除法对于除数为0时运行时将报错,但编译不会报错。
溢出
要特别注意,整数由于存在范围限制,如果计算结果超出了范围,就会产生溢出,而溢出不会出错,却会得到一个奇怪的结果:
public class HelloWorld {
public static void main(String []args) {
int x = 2147483640;
int y = 15;
int sum = x+y;
System.out.println(sum); // -2147483641
}
}
移位运算
对byte
和short
类型进行移位时,会首先转换为int
再进行位移。
仔细观察可发现,左移实际上就是不断地×2,右移实际上就是不断地÷2。
位运算
位运算是按位进行与、或、非和异或的运算。
与运算的规则是,必须两个数同时为1
,结果才为1
n = 0 & 0; // 0
或运算的规则是,只要任意一个为1
,结果就为1
n = 0 | 0; // 0
非运算的规则是,0
和1
互换
n = ~0; // 1
异或运算的规则是,如果两个数不同,结果为1
,否则为`0``
``n = 0 ^ 0; // 0`
运算优先级
在Java的计算表达式中,运算优先级从高到低依次是:
()
!
~
++
—
*
/
%
+
-
<<
>>
>>>
&
|
+=
-=
*=
/=
记不住也没关系,只需要加括号就可以保证运算的优先级正确。
浮点数运算
浮点数运算和整数运算相比,只能进行加减乘除这些数值计算,不能做位运算和移位运算。
由于浮点数存在运算误差,所以比较两个浮点数是否相等常常会出现错误的结果。正确的比较方法是判断两个浮点数之差的绝对值是否小于一个很小的数:
// 比较x和y是否相等,先计算其差的绝对值:
double r = Math.abs(x - y);
// 再判断绝对值是否足够小:
if (r < 0.00001) {
// 可以认为相等
} else {
// 不相等
}
字符和字符串
字符类型
字符类型char
是基本数据类型,它是character
的缩写。一个char
保存一个Unicode字符:
char c1 = 'A';
char c2 = '中';
还可以直接用转义字符u
+Unicode编码来表示一个字符:
// 注意是十六进制:
char c3 = 'u0041'; // 'A',因为十六进制0041 = 十进制65
char c4 = 'u4e2d'; // '中',因为十六进制4e2d = 十进制20013
字符串类型
字符串类型String
是引用类型,我们用双引号"…"
表示字符串。一个字符串可以存储0个到任意个字符:
String s = ""; // 空字符串,包含0个字符
String s1 = "A"; // 包含一个字符
String s2 = "ABC"; // 包含3个字符
String s3 = "中文 ABC"; // 包含6个字符,其中有一个空格
不可变特性:
观察执行结果,难道字符串s
变了吗?其实变的不是字符串,而是变量s
的“指向”。
执行String s = "hello";
时,JVM虚拟机先创建字符串"hello"
,然后,把字符串变量s
指向它:
紧接着,执行s = "world";
时,JVM虚拟机先创建字符串"world"
,然后,把字符串变量s
指向它:
来的字符串"hello"
还在,只是我们无法通过变量s
访问它而已。因此,字符串的不可变是指字符串内容不可变。
理解了引用类型的“指向”后,试解释下面的代码输出:
public class HelloWorld {
public static void main(String []args) {
String s = "kk";
String t = s;
s = "哈哈";
System.out.println(t); // kk
}
}
数组类型
Java的数组有几个特点:
- 数组所有元素初始化为默认值,整型都是
0
,浮点型是0.0
,布尔型是false
; - 数组一旦创建后,大小就不可改变。
也可以在定义数组时直接指定初始化的元素,这样就不必写出数组大小,而是由编译器自动推算数组大小。
数组元素可以是值类型(如int)或引用类型(如String),但数组本身是引用类型;
值类型
/*创建数组*/
int[] ns = new int[5];
int[] ns = new int[] {6,5,1,4};
//还可以进一步简写为:
int[] ns = { 68, 79, 91, 85, 62 };
注意数组是引用类型,并且数组大小不可变;
实际上它指向一个_新的_3个元素的数组;
但是,原有的5个元素的数组并没有改变,只是无法通过变量ns
引用到它们而已。
字符串数组
String[] names = {
"ABC", "XYZ", "zoo"
};
public class HelloWorld {
public static void main(String []args) {
String[] names = {"ABC","xyz","zoo"};
String s = names[1];
names[1] = "cat";
System.out.println(s); // xyz
}
}
流程控制
输入和输出
System.out.println(); // 换行
System.out.print(); // 不换行
格式化输出:
printf();
public class HelloWorld {
public static void main(String []args) {
double d = 3.1415926;
System.out.printf("%.2f
",d); // %f格式化输出浮点数
System.out.printf("%.4f
",d);
}
}
输入
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in); // 创建Scanner对象
System.out.print("Input your name: "); // 打印提示
String name = scanner.nextLine(); // 读取一行输入并获取字符串
System.out.print("Input your age: "); // 打印提示
int age = scanner.nextInt(); // 读取一行输入并获取整数
System.out.printf("Hi, %s, you are %d
", name, age); // 格式化输出
}
}
if判断
当if
语句块只有一行语句时,可以省略花括号{}
不推荐忽略花括号的写法。
判断引用类型相等
判断值类型的变量是否相等,可以使用==
运算符。但是,判断引用类型的变量是否相等,==
表示“引用是否相等”,或者说,是否指向同一个对象.例如,下面的两个String类型,它们的内容是相同的,但是,分别指向不同的对象,用==
判断,结果为false
:
package com.java.demo1;
import java.lang.reflect.Array;
public class HelloWorld {
public static void main(String []args) {
String s1 = "hello";
String s2 = "HELLO".toLowerCase();
System.out.println(s1); // hello
System.out.println(s2); //hello
if (s1 == s2) {
System.out.println("s1==s2");
} else {
System.out.println("s1!=s2"); //s1!=s2
}
// 使用equals()
if(s1.equals(s2)){
System.out.println("s1==s2");
} else {
System.out.println("s1!=s2"); //s1==s2
}
}
}
要判断引用类型的变量内容是否相等,必须使用equals()
方法:
switch多重选择
public class HelloWorld {
public static void main(String []args) {
String fruit = "apple";
switch (fruit) {
case "apple":
System.out.println("选择了apple");
break;
default:
System.out.println("啥也没选");
break;
}
}
}
简写:从Java 12开始,switch
语句升级为更简洁的表达式语法
yield语句返回值
从Java 13开始,switch
语句升级为表达式,不再需要break
,并且允许使用yield
返回值。
while循环
while
循环先判断循环条件是否满足,再执行循环语句;
while
循环可能一次都不执行;
编写循环时要注意循环条件,并避免死循环。
public class HelloWorld {
public static void main(String []args) {
int sum = 10;
int n = 1;
while (n<=100){
sum +=n;
n++;
}
System.out.println(sum); // 5060
}
}java
do while循环
do while
循环会至少循环一次。
先执行循环,再判断条件,条件满足时继续循环,条件不满足时退出。它的用法是:
public class HelloWorld {
public static void main(String []args) {
int sum = 10;
int n = 1;
do{
sum+=n;
n++;
} while (n<=100);
System.out.println(sum); // 5060
}
}
for循环
for each循环
int[] ns = {1,2,3,4};
for (int n : ns) {
System.out.println(n); // 1 2 3 4
}
除了数组外,for each
循环能够遍历所有“可迭代”的数据类型,包括后面会介绍的List
、Map
等。
和for
循环相比,for each
循环的变量n不再是计数器,而是直接对应到数组的每个元
break和continue
在循环过程中,可以使用break
语句跳出当前循环。我们来看一个例子:
public class HelloWorld {
public static void main(String []args) {
for (int i = 0; i < 10; i++) {
System.out.println(i);
for (int j = 0; j<=10; j++) {
System.out.println(j);
if (j>=1){
break; // break语句总是跳出自己所在的那一层循环
}
}
// break跳到这里
System.out.println("breaked");
}
}
}
break
会跳出当前循环,也就是整个循环都不会执行了
而continue
则是提前结束本次循环,直接继续执行下次循环。我们看一个例子:
public class HelloWorld {
public static void main(String []args) {
int sum = 0;
for (int i = 0; i < 10; i++) {
System.out.println("begin i="+i);
if (i%2 == 0) {
continue; // continue语句会结束本次循环
}
sum = sum + i;
System.out.println("end i =" +i);
}
System.out.println(sum);
}
}
数组操作
遍历数组
for和for each遍历
Java标准库提供了Arrays.toString()
,可以快速打印数组内容:
public class HelloWorld {
public static void main(String []args) {
int[] ns = {1,2,3,4,5,6};
System.out.println(Arrays.toString(ns)); // [1, 2, 3, 4, 5, 6]
}
}
数组排序
常用的排序算法有冒泡排序、插入排序和快速排序等
我们来看一下如何使用冒泡排序算法对一个整型数组从小到大进行排序:
public class HelloWorld {
public static void main(String []args) {
int[] ns = {22,26,13,64,51,36};
// 排序前
System.out.println(Arrays.toString(ns)); // [22, 26, 13, 64, 51, 36]
for (int i = 0; i < ns.length - 1; i++) {
for (int j = 0; j<ns.length - i - 1;j++){
if (ns[j]>ns[j+1]){
// 交换ns[j]和ns[j+1]
int tmp = ns[j];
ns[j] = ns[j+1];
ns[j+1] = tmp;
}
}
}
// 排序后
System.out.println(Arrays.toString(ns)); // [13, 22, 26, 36, 51, 64]
}
}
实际上,Java的标准库已经内置了排序功能,我们只需要调用JDK提供的Arrays.sort()
就可以排序:
必须注意,对数组排序实际上修改了数组本身
如果对一个字符串数组进行排序,3个字符串在内存中均没有任何变化,但是ns
数组的每个元素指向变化了。
多维数组
二维数组定义:
int[][] ns = {
{1,2,3},
{4,5,6}
}
要打印一个二维数组,可以使用两层嵌套的for循环:
for (int[] arr : ns) {
for (int n : arr) {
System.out.print(n);
System.out.print(', ');
}
System.out.println();
}
或者使用Java标准库的Arrays.deepToString()
:
三维数组
int[][][] ns = {
{
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
},
{
{10, 11},
{12, 13}
},
{
{14, 15, 16},
{17, 18}
}
};
命令行参数
Java程序的入口是main
方法,而main
方法可以接受一个命令行参数,它是一个String[]
数组。
public class Main {
public static void main(String[] args) {
for (String arg : args) {
if ("-version".equals(arg)) {
System.out.println("v 1.0");
break;
}
}
}
}
这样,程序就可以根据传入的命令行参数,作出不同的响应。
面向对象编程
面向对象基础
面向对象编程,是一种通过对象的方式,把现实世界映射到计算机模型的一种编程方法。
现实世界中,我们定义了“人”这种抽象概念,而具体的人则是“小明”、“小红”、“小军”等一个个具体的人。所以,“人”可以定义为一个类(class),而具体的人则是实例(instance):
*定义class
class Person {
public String name;
public int age;
}
*创建实例
Person ming = new Person();
有了指向这个实例的变量,我们就可以通过这个变量来操作实例。访问实例变量可以用变量.字段
,例如:
ming.name = "Xiao Ming"; // 对字段name赋值
ming.age = 12; // 对字段age赋值
System.out.println(ming.name); // 访问字段name
Person hong = new Person();
hong.name = "Xiao Hong";
hong.age = 15;
两个instance
拥有class
定义的name
和age
字段,且各自都有一份独立的数据,互不干扰。
方法
定义方法:
修饰符 方法返回类型 方法名(方法参数列表) {
若干方法语句;
return 方法返回值;
}
方法返回值通过return
语句实现,如果没有返回值,返回类型设置为void
,可以省略return
。
this变量:在方法内部,可以使用一个隐含的变量this
,它始终指向当前实例。因此,通过this.field
就可以访问当前实例的字段。
如果没有命名冲突,可以省略this
。例如:
class Person {
private String name;
public String getName() {
return name; // 相当于this.name
}
}
但是,如果有局部变量和字段重名,那么局部变量优先级更高,就必须加上this
:
class Person {
private String name;
public void setName(String name) {
this.name = name; // 前面的this不可少,少了就变成局部变量name了
}
}
方法参数:可以包含0个或任意个参数。
参数绑定:
public和private
修饰的field
有什么效果:
public class HelloWorld {
public static void main(String []args) {
Person ming = new Person();
ming.name = "666";
// ming.age = 123;
System.out.println(ming.name);
// System.out.println(ming.age);
}
}
class Person {
public String name;
private int age;
}
把field
从public
改成private
,外部代码不能访问这些field
,那我们定义这些field
有什么用?怎么才能给它赋值?怎么才能读取它的值?
所以我们需要使用方法(method
)来让外部代码可以间接修改field
:
public class HelloWorld {
public static void main(String []args) {
Person ming = new Person();
ming.setName("hhh");
// ming.setAge(12);
System.out.println(ming.getName()+","+ming.getAge()); // hhh,0
}
}
class Person {
public String name;
private int age;
public String getName(){
return this.name;
}
public void setName(String name) {
this.name = name;
}
public int getAge(){
return this.age;
}
public void setAge(int age) throws IllegalAccessException {
if (age<0||age>100){
throw new IllegalAccessException("超出年龄限制");
}
this.age = age;
}
}
对setName()
方法同样可以做检查,例如,不允许传入null
和空字符串:
public void setName(String name) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("invalid name");
}
this.name = name.strip(); // 去掉首尾空格
}
参数绑定
不可变:基本类型参数的传递,是调用方值的复制。双方各自的后续修改,互不影响。
可变:引用类型参数的传递,调用方的变量,和接收方的参数变量,指向的是同一个对象。双方任意一方对这个对象的修改,都会影响对方(因为指向同一个对象嘛)。
构造方法
在创建对象实例时就把内部字段全部初始化为合适的值
那前面我们并没有为Person
类编写构造方法,为什么可以调用new Person()
?
原因是如果一个类没有定义构造方法,编译器会自动为我们生成一个默认构造方法,它没有参数,也没有执行语句,类似这样:
class Person {
public Person() {
}
}
要特别注意的是,如果我们自定义了一个构造方法,那么,编译器就不再自动创建默认构造方法:
如果既要能使用带参数的构造方法,又想保留不带参数的构造方法,那么只能把两个构造方法都定义出来:
没有在构造方法中初始化字段时,引用类型的字段默认是null
,数值类型的字段用默认值,int
类型默认值是0
,布尔类型默认值是false
class Person {
//对字段直接进行初始化
private String name = "Unamed"; // 先初始化字段
private int age = 10;
public Person(String name, int age) { // 后执行构造方法的代码进行初始化。
this.name = name;
this.age = age;
}
}
多构造方法
一个构造方法可以调用其他构造方法,这样做的目的是便于代码复用。调用其他构造方法的语法是this(…)
:
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public Person(String name) {
this(name, 18); // 调用另一个构造方法Person(String, int)
}
public Person() {
this("Unnamed"); // 调用另一个构造方法Person(String)
}
}
方法重载
类似于构造
在一个类中,我们可以定义多个方法。如果有一系列方法,它们的功能都是类似的,只有参数有所不同,那么,可以把这一组方法名做成同名方法。例如,在Hello
类中,定义多个hello()
方法
这种方法名相同,但各自的参数不同,称为方法重载(Overload
)。
继承
继承是面向对象编程中非常强大的一种机制,它首先可以复用代码。当我们让Student
从Person
继承时,Student
就获得了Person
的所有功能,我们只需要为Student
编写新增的功能。
class Person {
private String name;
private int age;
public String getName() {...}
public void setName(String name) {...}
public int getAge() {...}
public void setAge(int age) {...}
}
class Student extends Person {
// 不要重复name和age字段/方法,
// 只需要定义新增score字段/方法:
private int score;
public int getScore() { … }
public void setScore(int score) { … }
}
在OOP的术语中,我们把Person
称为超类(super class),父类(parent class),基类(base class),把Student
称为子类(subclass),扩展类(extended class)。
Java只允许一个class继承自一个类,因此,一个类有且仅有一个父类。只有Object
特殊,它没有父类。
protected
继承有个特点,就是子类无法访问父类的private
字段或者private
方法。例如,Student
类就无法访问Person
类的name
和age
字段:
class Person{
private String name;
private int age;
}
class Student extends Person{
public String hello(){
return"Hello, "+ name;// 编译错误:无法访问name字段
}
}
这使得继承的作用被削弱了。为了让子类可以访问父类的字段,我们需要把private
改为protected
。用protected
修饰的字段可以被子类访问
super
super
关键字表示父类(超类)。子类引用父类的字段时,可以用super.fieldName
。例如:
实际上,这里使用super.name
,或者this.name
,或者name
,效果都是一样的。编译器会自动定位到父类的name
字段。
向上转型
向上转型实际上是把一个子类型安全地变为更加抽象的父类型:
Student s =newStudent();
Person p = s;// upcasting, ok
Object o1 = p;// upcasting, ok
Object o2 = s;// upcasting, ok
向下转型
和向上转型相反,如果把一个父类类型强制转型为子类类型,就是向下转型
Person p1 =newStudent();// upcasting, ok
Person p2 =newPerson();
Student s1 =(Student) p1;// ok
Student s2 =(Student) p2;// runtime error! ClassCastException!
如果测试上面的代码,可以发现:
Person
类型p1
实际指向Student
实例,Person
类型变量p2
实际指向Person
实例。在向下转型的时候,把p1
转型为Student
会成功,因为p1
确实指向Student
实例,把p2
转型为Student
会失败,因为p2
的实际类型是Person
,不能把父类变为子类,因为子类功能比父类多,多的功能无法凭空变出来。
因此,向下转型很可能会失败。失败的时候,Java虚拟机会报ClassCastException
。
为了避免向下转型出错,Java提供了instanceof
操作符,可以先判断一个实例究竟是不是某种类型:
Person p = new Student();
if(p instanceof Student){
// 只有判断成功才会向下转型:
Student s = (Student) p;// 一定会成功
}
区分继承和组合
多态
在继承关系中,子类如果定义了一个与父类方法签名完全相同的方法,被称为覆写(Override)。
Override和Overload不同的是,如果方法签名如果不同,就是Overload,Overload方法是一个新方法;如果方法签名相同,并且返回值也相同,就是Override
。
注意:方法名相同,方法参数相同,但方法返回值不同,也是不同的方法。在Java程序中,出现这种情况,编译器会报错。
那么,一个实际类型为Student
,引用类型为Person
的变量,调用其run()
方法,调用的是Person
还是Student
的run()
方法?
运行一下上面的代码就可以知道,实际上调用的方法是Student
的run()
方法。因此可得出结论:
Java的实例方法调用是基于运行时的实际类型的动态调用,而非变量的声明类型。
这个非常重要的特性在面向对象编程中称之为多态。它的英文拼写非常复杂:Polymorphic。
多态的特性就是,运行期才能动态决定调用的子类方法。
final
继承可以允许子类覆写父类的方法。如果一个父类不允许子类对它的某个方法进行覆写,可以把该方法标记为final
。用final
修饰的方法不能被Override
:
class Person {
protected String name;
public final String hello() {
return "Hello, " + name;
}
}
Student extends Person {
// compile error: 不允许覆写
@Override
public String hello() {
}
}
如果一个类不希望任何其他类继承自它,那么可以把这个类本身标记为final
。用final
修饰的类不能被继承:
final class Person {
protected String name;
}
// compile error: 不允许继承自Person
Student extends Person {
}
抽象类
把一个方法声明为abstract
,表示它是一个抽象方法,本身没有实现任何方法语句。因为这个抽象方法本身是无法执行的,所以,Person
类也无法被实例化。编译器会告诉我们,无法编译Person
类,因为它包含抽象方法。
abstract class Person {
public abstract void run();
}
如果一个class
定义了方法,但没有具体执行代码,这个方法就是抽象方法,抽象方法用abstract
修饰。
因为无法执行抽象方法,因此这个类也必须申明为抽象类(abstract class)。
使用abstract
修饰的类就是抽象类。我们无法实例化一个抽象类:
Person p = new Person(); // 编译错误
Person
类定义了抽象方法run()
,那么,在实现子类Student
的时候,就必须覆写run()
方法:
public class HelloWorld {
public static void main(String []args) {
Person p = new Student();
p.run(); // 学生类覆写run
}
}
abstract class Person {
public abstract void run();
}
class Student extends Person {
@Override
public void run() {
System.out.println("学生类覆写run");
}
}
面向抽象编程
这种尽量引用高层类型,避免引用实际子类型的方式,称之为面向抽象编程。
小结
- 通过
abstract
定义的方法是抽象方法,它只有定义,没有实现。抽象方法定义了子类必须实现的接口规范; - 定义了抽象方法的class必须被定义为抽象类,从抽象类继承的子类必须实现抽象方法;
- 如果不实现抽象方法,则该子类仍是一个抽象类;
- 面向抽象编程使得调用者只关心抽象方法的定义,不关心子类的具体实现
接口
在抽象类中,抽象方法本质上是定义接口规范:即规定高层类的接口,从而保证所有子类都有相同的接口实现,这样,多态就能发挥出威力。
如果一个抽象类没有字段,所有方法全部都是抽象方法:
abstract class Person {
public abstract void run();
public abstract String getName();
}
就可以把该抽象类改写为接口:interface
。
在Java中,使用interface
可以声明一个接口:
interface Person{
void run();
String getName();
}
当一个具体的class
去实现一个interface
时,需要使用implements
关键字。举个例子:
class Student implementsPerson {
private String name;
public Student(String name) {
this.name = name;
}
@Override
public void run(){
System.out.println(this.name +" run");
}
@Override
public String getName(){
returnthis.name;
}
}
我们知道,在Java中,一个类只能继承自另一个类,不能从多个类继承。但是,一个类可以实现多个interface
,例如:
class Student implements Person,Hello{// 实现了两个interface
...
}
接口继承
一个interface
可以继承自另一个interface
。interface
继承自interface
使用extends
,它相当于扩展了接口的方法。例如:
interface Hello{
void hello();
}
interface Person extends Hello{
void run();
String getName();
}
default方法
在接口中,可以定义default
方法。例如,把Person
接口的run()
方法改为default
方法:
public class HelloWorld {
public static void main(String []args) {
Person p = new Student("xiao wang ba");
p.run(); // xiao wang barun
}
}
interface Person {
String getName();
default void run() {
System.out.println(getName() + "run");
}
}
class Student implements Person {
private String name;
public Student(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
}
小结
Java的接口(interface)定义了纯抽象规范,一个类可以实现多个接口;
接口也是数据类型,适用于向上转型和向下转型;
接口的所有方法都是抽象方法,接口不能定义实例字段;
接口可以定义default
方法(JDK>=1.8)
静态字段
在一个class
中定义的字段,我们称之为实例字段。实例字段的特点是,每个实例都有独立的字段,各个实例的同名字段互不影响。
还有一种字段,是用static
修饰的字段,称为静态字段:static field
。
实例字段在每个实例中都有自己的一个独立“空间”,但是静态字段只有一个共享“空间”,所有实例都会共享该字段。举个例子:
class Person {
public String name;
public int age;
// 定义静态字段number:
public static int number;
}
public class HelloWorld {
public static void main(String[] args) {
Person ming = new Person("xaiowangba", 12);
Person hong = new Person("dawangba", 16);
ming.number = 666;
System.out.println(ming.number); // 666
System.out.println(hong.number); // 666
hong.number = 888;
System.out.println(ming.number); // 888
System.out.println(hong.number); // 888
}
}
class Person {
public String name;
public int age;
public static int number;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
推荐用类名来访问静态字段。
Person.number = 999;
System.out.println(Person.number);
静态方法
有静态字段,就有静态方法。用static
修饰的方法称为静态方法。
调用实例方法必须通过一个实例变量,而调用静态方法则不需要实例变量,通过类名就可以调用。静态方法类似其它编程语言的函数。例如:
public class HelloWorld {
public static void main(String[] args) {
Person.setNumber(99);
System.out.println(Person.number); // 99
}
}
class Person {
public static int number;
public static void setNumber(int value) {
number = value;
}
}
接口的静态字段
因为interface
是一个纯抽象类,所以它不能定义实例字段。但是,interface
是可以有静态字段的,并且静态字段必须为final
类型:
public interface Person {
public static final int MALE = 1;
public static final int FEMALE = 2;
}
实际上,因为interface
的字段只能是public static final
类型,所以我们可以把这些修饰符都去掉,上述代码可以简写为:
public interface Person {
// 编译器会自动加上public statc final:
int MALE = 1;
int FEMALE = 2;
}
静态方法常用于工具类和辅助方法。
包
Java定义了一种名字空间,称之为包:package
。一个类总是属于某个包,类名(比如Person
)只是一个简写,真正的完整类名是包名.类名
。
文件结构
package_sample
└─ src
├─ hong
│ └─ Person.java
│ ming
│ └─ Person.java
└─ mr
└─ jun
└─ Arrays.java
Person.java
文件:
package ming; // 申明包名ming
public class Person {
}
Arrays.java
文件:
package mr.jun; // 申明包名mr.jun
public class Arrays {
}
包作用域
位于同一个包的类,可以访问包作用域的字段和方法。不用public
、protected
、private
修饰的字段和方法就是包作用域。例如,Person
类定义在hello
包下面:
package hello;
public class Person {
// 包作用域:
void hello() {
System.out.println("Hello!");
}
}
//Main类也定义在hello包下面:
public class Main {
public static void main(String[] args) {
Person p = new Person();
p.hello(); // 可以调用,因为Main和Person在同一个包
}
}
import
第一种,(不推荐)直接写出完整类名,例如:
// Person.java
package ming;
public class Person {
public void run() {
mr.jun.Arrays arrays = new mr.jun.Arrays();
}
}
因此,第二种写法是用import
语句,导入小军的Arrays
,然后写简单类名:
// Person.java
package ming;
// 导入完整类名:
import mr.jun.Arrays;
public class Person {
public void run() {
Arrays arrays = new Arrays();
}
}
在写import
的时候,可以使用*
,表示把这个包下面的所有class
都导入进来(但不包括子包的class
):
// Person.java
package ming;
// 导入mr.jun包的所有class:
import mr.jun.*;
public class Person {
public void run() {
Arrays arrays = new Arrays();
}
}
还有一种import static
的语法(很少用),它可以导入可以导入一个类的静态字段和静态方法:
package main;
// 导入System类的所有静态字段和静态方法:
import static java.lang.System.*;
public class Main {
public static void main(String[] args) {
// 相当于调用System.out.println(…)
out.println("Hello, world!");
}
作用域
在Java中,我们经常看到public
、protected
、private
这些修饰符。在Java中,这些修饰符可以用来限定访问作用域。
public
定义为public
的class
、interface
可以被其他任何类访问:
private
定义为private
的field
、method
无法被其他类访问:
package abc;
public class Hello {
// 不能被其他类调用:
private void hi() {
}
public void hello() {
this.hi(); // 报错
}
}
由于Java支持嵌套类,如果一个类内部还定义了嵌套类,那么,嵌套类拥有访问private
的权限:
public class HelloWorld {
public static void main(String[] args) {
Inner i = new Inner();
i.hi(); // 私有方法hello调用
}
// private方法
private static void hello(){
System.out.println("私有方法hello调用");
}
// 静态内部类
static class Inner{
public void hi(){
HelloWorld.hello();
}
}
}
protected
protected
作用于继承关系。定义为protected
的字段和方法可以被子类访问,以及子类的子类:
package abc;
public class Hello {
// protected方法:
protected void hi() {
}
}
上面的protected
方法可以被继承的类访问:
package xyz;
class Main extends Hello {
void foo() {
Hello h = new Hello();
// 可以访问protected方法:
h.hi();
}
}
package
最后,包作用域是指一个类允许访问同一个package
的没有public
、private
修饰的class
,以及没有public
、protected
、private
修饰的字段和方法。
package abc;
// package权限的类:
class Hello {
// package权限的方法:
void hi() {
}
}
只要在同一个包,就可以访问package
权限的class
、field
和method
:
package abc;
class Main {
void foo() {
// 可以访问package权限的类:
Hello h = new Hello();
// 可以调用package权限的方法:
h.hi();
}
局部变量
在方法内部定义的变量称为局部变量,局部变量作用域从变量声明处开始到对应的块结束。方法参数也是局部变量。
final
Java还提供了一个final
修饰符。final
与访问权限不冲突,它有很多作用。
用final
修饰class
可以阻止被继承:
package abc;
// 无法被继承:
public final class Hello {
private int n = 0;
protected void hi(int t) {
long i = t;
}
}
用final
修饰method
可以阻止被子类覆写:
package abc;
public class Hello {
// 无法被覆写:
protected final void hi() {
}
}
用final
修饰field
可以阻止被重新赋值:
package abc;
public class Hello {
private final int n = 0;
protected void hi() {
this.n = 1; // error!Cannot assign a value to final variable 'n'
}
}
用final
修饰局部变量可以阻止被重新赋值:
package abc;
public class Hello {
protected void hi(final int t) {
t = 1; // error!
}
}
classpath和jar
小结
JVM通过环境变量classpath
决定搜索class
的路径和顺序;
不推荐设置系统环境变量classpath
,始终建议通过-cp
命令传入;
jar包相当于目录,可以包含很多.class
文件,方便下载和使用;
MANIFEST.MF
文件可以提供jar包的信息,如Main-Class
,这样可以直接运行jar包。
模块Module
从Java 9开始,JDK又引入了模块(Module)。
什么是模块?这要从Java 9之前的版本说起。
从Java 9开始引入的模块,主要是为了解决“依赖”这个问题。如果a.jar
必须依赖另一个b.jar
才能运行,那我们应该给a.jar
加点说明啥的,让程序在编译和运行的时候能自动定位到b.jar
,这种自带“依赖关系”的class容器就是模块。
编写模块
那么,我们应该如何编写模块呢?还是以具体的例子来说。首先,创建模块和原有的创建Java项目是完全一样的,以oop-module
工程为例,它的目录结构如下:
oop-module
├── bin
├── build.sh
└── src
├── com
│ └── itranswarp
│ └── sample
│ ├── Greeting.java
│ └── Main.java
└── module-info.java
其中,bin
目录存放编译后的class文件,src
目录存放源码,按包名的目录结构存放,仅仅在src
目录下多了一个module-info.java
这个文件,这就是模块的描述文件。在这个模块中,它长这样:
module hello.world {
requires java.base; // 可不写,任何模块都会自动引入java.base
requires java.xml;
}
其中,module
是关键字,后面的hello.world
是模块的名称,它的命名规范与包一致。花括号的requires xxx;
表示这个模块需要引用的其他模块名。除了java.base
可以被自动引入外,这里我们引入了一个java.xml
的模块。
当我们使用模块声明了依赖关系后,才能使用引入的模块。例如,Main.java
代码如下:
package com.itranswarp.sample;
// 必须引入java.xml模块后才能使用其中的类:
import javax.xml.XMLConstants;
public class Main {
public static void main(String[] args) {
Greeting g = new Greeting();
System.out.println(g.hello(XMLConstants.XML_NS_PREFIX));
}
}
运行模块
打包JRE
访问权限
小结
Java 9引入的模块目的是为了管理依赖;
使用模块可以按需打包JRE;
使用模块对类的访问权限有了进一步限制。
Java核心类
字符串String
在Java中,String
是一个引用类型,它本身也是一个class
。但是,Java编译器对String
有特殊处理,即可以直接用"…"
来表示一个字符串:
String s1 = "Hello!";
实际上字符串在String
内部是通过一个char[]
数组表示的,因此,按下面的写法也是可以的:
String s2 = new String(new char[] {'H', 'e', 'l', 'l', 'o', '!'});
字符串比较
必须使用equals()
方法而不能用==
。
要忽略大小写比较,使用equalsIgnoreCase()
方法。
// 是否包含子串:
"Hello".contains("ll"); // true
搜索子串
"Hello".indexOf("l"); // 2
"Hello".lastIndexOf("l"); // 3
"Hello".startsWith("He"); // true
"Hello".endsWith("lo"); // true
提取子串
"Hello".substring(2); // "llo"
"Hello".substring(2, 4); "ll"
去除首尾空白字符
// 使用trim()方法可以移除字符串首尾空白字符。空白字符包括空格, ,
,
:
" Hello
".trim(); // "Hello"
注意:trim()
并没有改变字符串的内容,而是返回了一个新字符串。
另一个strip()
方法也可以移除字符串首尾空白字符。它和trim()
不同的是,类似中文的空格字符u3000
也会被移除:
"u3000Hellou3000".strip(); // "Hello"
" Hello ".stripLeading(); // "Hello "
" Hello ".stripTrailing(); // " Hello"
判断字符串是否为空和空白字符串
"".isEmpty(); // true,因为字符串长度为0
" ".isEmpty(); // false,因为字符串长度不为0
"
".isBlank(); // true,因为只包含空白字符
" Hello ".isBlank(); // false,因为包含非空白字符
替换子串
String s = "hello";
s.replace('l', 'w'); // "hewwo",所有字符'l'被替换为'w'
s.replace("ll", "~~"); // "he~~o",所有子串"ll"被替换为"~~"
另一种是通过正则表达式替换:
String s = "A,,B;C ,D";
s.replaceAll("[\,\;\s]+", ","); // "A,B,C,D"
分割字符串
要分割字符串,使用split()
方法,并且传入的也是正则表达式:
String s = "A,B,C,D";
String[] ss = s.split("\,"); // {"A", "B", "C", "D"}
拼接字符串
拼接字符串使用静态方法join()
,它用指定的字符串连接字符串数组:
String[] arr = {"A", "B", "C"};
String s = String.join("***", arr); // "A***B***C"
类型转换
把任意基本类型或引用类型转换为字符串,可以使用静态方法valueOf()
。这是一个重载方法,编译器会根据参数自动选择合适的方法:
String.valueOf(123); // "123"
String.valueOf(45.67); // "45.67"
String.valueOf(true); // "true"
String.valueOf(new Object()); // 类似java.lang.Object@636be97c
把字符串转换为int
类型:
int n1 = Integer.parseInt("123"); // 123
int n2 = Integer.parseInt("ff", 16); // 按十六进制转换,255
把字符串转换为boolean
类型:
boolean b1 = Boolean.parseBoolean("true"); // true
boolean b2 = Boolean.parseBoolean("FALSE"); // false
要特别注意,Integer
有个getInteger(String)
方法,它不是将字符串转换为int
,而是把该字符串对应的系统变量转换为Integer
:
Integer.getInteger("java.version"); // 版本号,11
转换为char[]
String
和char[]
类型可以互相转换,方法是:
char[] cs = "Hello".toCharArray(); // String -> char[]
String s = new String(cs); // char[] -> String
如果修改了char[]
数组,String
并不会改变:
public class HelloWorld {
public static void main(String[] args) {
char[] cs = "Hello".toCharArray();
System.out.println(cs); // Hello
String s = new String(cs);
System.out.println(s); // Hello
cs[0] = 'X';
System.out.println(cs); // Xello
System.out.println(s); // Hello
}
}
这是因为通过new String(char[])
创建新的String
实例时,它并不会直接引用传入的char[]
数组,而是会复制一份,所以,修改外部的char[]
数组不会影响String
实例内部的char[]
数组,因为这是两个不同的数组。
从String
的不变性设计可以看出,如果传入的对象有可能改变,我们需要复制而不是直接引用。
public class HelloWorld {
public static void main(String[] args) {
int[] scores = new int[] {88,77,51,66};
Score s = new Score(scores);
s.printScores(); // [88, 77, 51, 66]
scores[2] = 99;
s.printScores(); // [88, 77, 99, 66]
}
}
class Score {
private int[] scores;
public Score(int[] scores) {
this.scores = scores;
}
public void printScores(){
System.out.println(Arrays.toString(scores));
}
}
观察两次输出,由于Score
内部直接引用了外部传入的int[]
数组,这会造成外部代码对int[]
数组的修改,影响到Score
类的字段。如果外部代码不可信,这就会造成安全隐患。
字符编码
Unicode
编码需要两个或者更多字节表示,我们可以比较中英文字符在ASCII
、GB2312
和Unicode
的编码:
英文字符'A'
的ASCII
编码和Unicode
编码:
┌────┐
ASCII: │ 41 │
└────┘
┌────┬────┐
Unicode: │ 00 │ 41 │
└────┴────┘
UTF-8
编码的另一个好处是容错能力强。如果传输过程中某些字符出错,不会影响后续字符,因为UTF-8
编码依靠高字节位来确定一个字符究竟是几个字节,它经常用来作为传输编码。
在Java中,char
类型实际上就是两个字节的Unicode
编码。如果我们要手动把字符串转换成其他编码,可以这样做:
byte[] b1 = "Hello".getBytes(); // 按ISO8859-1编码转换,不推荐
byte[] b2 = "Hello".getBytes("UTF-8"); // 按UTF-8编码转换
byte[] b2 = "Hello".getBytes("GBK"); // 按GBK编码转换
byte[] b3 = "Hello".getBytes(StandardCharsets.UTF_8); // 按UTF-8编码转换
注意:转换编码后,就不再是char
类型,而是byte
类型表示的数组。
如果要把已知编码的byte[]
转换为String
,可以这样做:
byte[] b = ...
String s1 = new String(b, "GBK"); // 按GBK转换
String s2 = new String(b, StandardCharsets.UTF_8); // 按UTF-8转换
始终牢记:Java的String
和char
在内存中总是以Unicode编码表示。
StringBuilder
Java编译器对String
做了特殊处理,使得我们可以直接用+
拼接字符串。
考察下面的循环代码:
String s = "";
for (int i = 0; i < 1000; i++) {
s = s + "," + i;
}
虽然可以直接拼接字符串,但是,在循环中,每次循环都会创建新的字符串对象,然后扔掉旧的字符串。这样,绝大部分字符串都是临时对象,不但浪费内存,还会影响GC效率。
为了能高效拼接字符串,Java标准库提供了StringBuilder
,它是一个可变对象,可以预分配缓冲区,这样,往StringBuilder
中新增字符时,不会创建新的临时对象:
StringBuilder sb = new StringBuilder(1024);
for (int i = 0; i < 1000; i++) {
sb.append(',');
sb.append(i);
}
String s = sb.toString();
对比:
public class HelloWorld {
public static void main(String[] args) {
long startTime=System.currentTimeMillis(); //获取开始时间
String s = "";
for (int i = 0; i < 10000; i++) {
s = s + "," + i;
}
long endTime=System.currentTimeMillis(); //获取结束时间
System.out.println("第一段程序运行时间: "+(endTime-startTime)+"ms"); // 第一段程序运行时间: 71ms
long startTimetwo=System.currentTimeMillis(); //获取开始时间
StringBuilder sb = new StringBuilder(1024);
for (int i = 0; i < 10000; i++) {
sb.append(',');
sb.append(i);
}
String s2 = sb.toString();
long endTimetwo=System.currentTimeMillis(); //获取结束时间
System.out.println("第二段程序运行时间: "+(endTimetwo-startTimetwo)+"ms"); // 第二段程序运行时间: 2ms
}
}
StringBuilder
还可以进行链式操作:
public class HelloWorld {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder(1024);
sb.append("Mr")
.append("Bob")
.append("!")
.insert(0,"hello,");
System.out.println(sb); // hello,MrBob!
}
}
注意:对于普通的字符串+
操作,并不需要我们将其改写为StringBuilder
,因为Java编译器在编译时就自动把多个连续的+
操作编码为StringConcatFactory
的操作。在运行期,StringConcatFactory
会自动把字符串连接操作优化为数组复制或者StringBuilder
操作。
你可能还听说过StringBuffer
,这是Java早期的一个StringBuilder
的线程安全版本,它通过同步来保证多个线程操作StringBuffer
也是安全的,但是同步会带来执行速度的下降。
StringBuilder
和StringBuffer
接口完全相同,现在完全没有必要使用StringBuffer
。
小结
StringBuilder
是可变对象,用来高效拼接字符串;
StringBuilder
可以支持链式操作,实现链式操作的关键是返回实例本身;
StringBuffer
是StringBuilder
的线程安全版本,现在很少使用。
StringJoiner
类似用分隔符拼接数组的需求很常见,所以Java标准库还提供了一个StringJoiner
来干这个事:
public class HelloWorld {
public static void main(String[] args) {
String[] names = {"Bob","Alice","Grace"};
// StringJoiner sj = new StringJoiner(",");
var sj = new StringJoiner(",");
for (String name : names) {
sj.add(name);
}
System.out.println(sj.toString()); // Bob,Alice,Grace
}
}java
慢着!用StringJoiner
的结果少了前面的"Hello "
和结尾的"!"
!遇到这种情况,需要给StringJoiner
指定“开头”和“结尾”:
public class HelloWorld {
public static void main(String[] args) {
String[] names = {"Bob","Alice","Grace"};
// StringJoiner sj = new StringJoiner(",");
var sj = new StringJoiner(",", "Hello ", "!");
for (String name : names) {
sj.add(name);
}
System.out.println(sj.toString()); // Hello Bob,Alice,Grace!
}
}
那么StringJoiner
内部是如何拼接字符串的呢?如果查看源码,可以发现,StringJoiner
内部实际上就是使用了StringBuilder
,所以拼接效率和StringBuilder
几乎是一模一样的。
String.join()
String
还提供了一个静态方法join()
,这个方法在内部使用了StringJoiner
来拼接字符串,在不需要指定“开头”和“结尾”的时候,用String.join()
更方便:
String[] names ={"Bob","Alice","Grace"};
var s =String.join(", ", names);
包装类型
我们已经知道,Java的数据类型分两种:
- 基本类型:
byte
,short
,int
,long
,boolean
,float
,double
,char
- 引用类型:所有
class
和interface
类型
引用类型可以赋值为null
,表示空,但基本类型不能赋值为null
:
String s = null;
int n = null; // compile error!
比如,想要把int
基本类型变成一个引用类型,我们可以定义一个Integer
类,它只包含一个实例字段int
,这样,Integer
类就可以视为int
的包装类(Wrapper Class):
public class Integer {
private int value;
public Integer(int value) {
this.value = value;
}
public int intValue() {
return this.value;
}
}
定义好了Integer
类,我们就可以把int
和Integer
互相转换:
Integer n = null;
Integer n2 = new Integer(99);
int n3 = n2.intValue();
实际上,因为包装类型非常有用,Java核心库为每种基本类型都提供了对应的包装类型:
基本类型 对应的引用类型
boolean java.lang.Boolean
byte java.lang.Byte
short java.lang.Short
int java.lang.Integer
long java.lang.Long
float java.lang.Float
double java.lang.Double
char java.lang.Character
我们可以直接使用,并不需要自己去定义:
public class HelloWorld {
public static void main(String[] args) {
int i = 100;
// 通过new操作符创建Integer实例(不推荐使用,会有编译警告)
Integer n1 = new Integer(i);
// 通过静态方法valueOf(int)创建Integer实例
Integer n2 = Integer.valueOf(i);
// 通过静态方法valueOf(String)创建Integer实例
Integer n3 = Integer.valueOf("100");
System.out.println(n3);
System.out.println(n3.intValue());
}
}
自动装箱
因为int
和Integer
可以互相转换:
int i = 100;
Integer n = Integer.valueOf(i);
int x = n.intValue();
所以,Java编译器可以帮助我们自动在int
和Integer
之间转型:
Integer n = 100; // 编译器自动使用Integer.valueOf(int)
int x = n; // 编译器自动使用Integer.intValue()
这种直接把int
变为Integer
的赋值写法,称为自动装箱(Auto Boxing),反过来,把Integer
变为int
的赋值写法,称为自动拆箱(Auto Unboxing)。
注意:自动装箱和自动拆箱只发生在编译阶段,目的是为了少写代码。
装箱和拆箱会影响代码的执行效率,因为编译后的class
代码是严格区分基本类型和引用类型的。并且,自动拆箱执行时可能会报NullPointerException
:
public class HelloWorld {
public static void main(String[] args) {
Integer n = null;
int i = n;
System.out.println(i);
}
}
错误
Exception in thread "main" java.lang.NullPointerException
不变类
所有的包装类型都是不变类。我们查看Integer
的源码可知,它的核心代码如下:
public final class Integer {
private final int value;
}
因此,一旦创建了Integer
对象,该对象就是不变的。
对两个Integer
实例进行比较要特别注意:绝对不能用==
比较,因为Integer
是引用类型,必须使用equals()
比较:
public class HelloWorld {
public static void main(String[] args) {
Integer x = 127;
Integer y = 127;
Integer m = 9999;
Integer n = 9999;
System.out.println("x==y: " + (x==y)); // true
System.out.println("m==n: " + (m==n)); // false
System.out.println("x.equals(y): " + x.equals(y)); // true
System.out.println("m.equals(n): " + m.equals(n)); // true
}
}
仔细观察结果的童鞋可以发现,==
比较,较小的两个相同的Integer
返回true
,较大的两个相同的Integer
返回false
,这是因为Integer
是不变类,编译器把Integer x = 127;
自动变为Integer x = Integer.valueOf(127);
,为了节省内存,Integer.valueOf()
对于较小的数,始终返回相同的实例,因此,==
比较“恰好”为true
,但我们绝不能因为Java标准库的Integer
内部有缓存优化就用==
比较,必须用equals()
方法比较两个Integer
。
按照语义编程,而不是针对特定的底层实现去“优化”。
因为Integer.valueOf()
可能始终返回同一个Integer
实例,因此,在我们自己创建Integer
的时候,以下两种方法:
- 方法1:
Integer n = new Integer(100);
- 方法2:
Integer n = Integer.valueOf(100);
方法2更好,因为方法1总是创建新的Integer
实例,方法2把内部优化留给Integer
的实现者去做,即使在当前版本没有优化,也有可能在下一个版本进行优化。
我们把能创建“新”对象的静态方法称为静态工厂方法。Integer.valueOf()
就是静态工厂方法,它尽可能地返回缓存的实例以节省内存。
创建新对象时,优先选用静态工厂方法而不是new操作符。
如果我们考察Byte.valueOf()
方法的源码,可以看到,标准库返回的Byte
实例全部是缓存实例,但调用者并不关心静态工厂方法以何种方式创建新实例还是直接返回缓存的实例。
进制转换
字符串解析成一个整数
int x1 = Integer.parseInt("100"); // 100
int x2 = Integer.parseInt("100", 16); // 256,因为按16进制解析 把100当成16进制-->转换为10进制256
Integer
还可以把整数格式化为指定进制的字符串:
public class HelloWorld {
public static void main(String[] args) {
System.out.println(Integer.toString(100)); // "100",表示为10进制
System.out.println(Integer.toString(100,36));// "2s",表示36进制
System.out.println(Integer.toHexString(100)); // "64", 表示16进制
System.out.println(Integer.toOctalString(100)); // "144", 8进制
System.out.println(Integer.toBinaryString(100)); // "1100100",2进制
}
}
注意:上述方法的输出都是String
,在计算机内存中,只用二进制表示,不存在十进制或十六进制的表示方法。int n = 100
在内存中总是以4字节的二进制表示:
┌────────┬────────┬────────┬────────┐
│00000000│00000000│00000000│01100100│
└────────┴────────┴────────┴────────┘
Java的包装类型还定义了一些有用的静态变量
// boolean只有两个值true/false,其包装类型只需要引用Boolean提供的静态字段:
Boolean t =Boolean.TRUE;
Boolean f =Boolean.FALSE;
// int可表示的最大/最小值:
int max =Integer.MAX_VALUE;// 2147483647
int min =Integer.MIN_VALUE;// -2147483648
// long类型占用的bit和byte数量:
int sizeOfLong =Long.SIZE;// 64 (bits)
int bytesOfLong =Long.BYTES;// 8 (bytes)
最后,所有的整数和浮点数的包装类型都继承自Number
,因此,可以非常方便地直接通过包装类型获取各种基本类型:
// 向上转型为Number:
Number num =newInteger(999);
// 获取byte, int, long, float, double:
byte b = num.byteValue();
int n = num.intValue();
long ln = num.longValue();
float f = num.floatValue();
double d = num.doubleValue();
JavaBean
JavaBean的作用
JavaBean主要用来传递数据,即把一组数据组合成一个JavaBean便于传输。此外,JavaBean可以方便地被IDE工具分析,生成读写属性的代码,主要用在图形界面的可视化设计中。
过IDE,可以快速生成getter
和setter
。例如,在Eclipse中,先输入以下代码:
public class Person {
private String name;
private int age;
}
然后,点击右键,在弹出的菜单中选择“Source”,“Generate Getters and Setters”,在弹出的对话框中选中需要生成getter
和setter
方法的字段,点击确定即可由IDE自动完成所有方法代码。
快捷键alt+enter、alt+insert。
枚举类
在Java中,我们可以通过static final
来定义常量。例如,我们希望定义周一到周日这7个常量,可以用7个不同的int
表示:
public class Weekday {
public static final int SUN = 0;
public static final int MON = 1;
public static final int TUE = 2;
public static final int WED = 3;
public static final int THU = 4;
public static final int FRI = 5;
public static final int SAT = 6;
}
使用常量的时候,可以这么引用:
if (day == Weekday.SAT || day == Weekday.SUN) {
// TODO: work at home
}
enum
为了让编译器能自动检查某个值在枚举的集合内,并且,不同用途的枚举需要不同的类型来标记,不能混用,我们可以使用enum
来定义枚举类:
public class HelloWorld {
public static void main(String[] args) {
Weekday day = Weekday.SUM;
if (day == Weekday.SAT || day ==Weekday.SUM) {
System.out.println("ppp");
}
}
}
enum Weekday {
SUM, MON, TUE, WED, THU, FRI, SAT
}
注意到定义枚举类是通过关键字enum
实现的,我们只需依次列出枚举的常量名。
enum的比较
使用enum
定义的枚举类是一种引用类型。前面我们讲到,引用类型比较,要使用equals()
方法,如果使用==
比较,它比较的是两个引用类型的变量是否是同一个对象。因此,引用类型比较,要始终使用equals()
方法,但enum
类型可以例外。
这是因为enum
类型的每个常量在JVM中只有一个唯一实例,所以可以直接用==
比较:
if (day == Weekday.FRI) { // ok!
}
if (day.equals(Weekday.SUN)) { // ok, but more code!
}
enum类型
name():
返回常量名,例如:
String s = Weekday.SUN.name(); // "SUN"
ordinal():
返回定义的常量的顺序,从0开始计数,例如:
int n = Weekday.MON.ordinal(); // 1
switch
最后,枚举类可以应用在switch
语句中。因为枚举类天生具有类型信息和有限个枚举常量,所以比int
、String
类型更适合用在switch
语句中:
加上default
语句,可以在漏写某个枚举常量时自动报错,从而及时发现错误。
BigInteger
在Java中,由CPU原生提供的整型最大范围是64位long
型整数。使用long
型整数可以直接通过CPU指令进行计算,速度非常快。
如果我们使用的整数范围超过了long
型怎么办?这个时候,就只能用软件来模拟一个大整数。java.math.BigInteger
就是用来表示任意大小的整数。BigInteger
内部用一个int[]
数组来模拟一个非常大的整数:
BigInteger bi = new BigInteger("1234567890");
System.out.println(bi.pow(5)); // 2867971860299718107233761438093672048294900000
对BigInteger
做运算的时候,只能使用实例方法,例如,加法运算:
BigInteger i1 = new BigInteger("1234567890");
BigInteger i2 = new BigInteger("12345678901234567890");
BigInteger sum = i1.add(i2); // 12345678902469135780
和long
型整数运算比,BigInteger
不会有范围限制,但缺点是速度比较慢。
也可以把BigInteger
转换成long
型:
BigInteger i = new BigInteger("123456789000");
System.out.println(i.longValue()); // 123456789000
System.out.println(i.multiply(i).longValueExact()); // java.lang.ArithmeticException: BigInteger out of long range
使用longValueExact()
方法时,如果超出了long
型的范围,会抛出ArithmeticException
。
BigInteger
和Integer
、Long
一样,也是不可变类,并且也继承自Number
类。因为Number
定义了转换为基本类型的几个方法:
- 转换为
byte
:byteValue()
- 转换为
short
:shortValue()
- 转换为
int
:intValue()
- 转换为
long
:longValue()
- 转换为
float
:floatValue()
- 转换为
double
:doubleValue()
BigDecimal
和BigInteger
类似,BigDecimal
可以表示一个任意大小且精度完全准确的浮点数。
BigDecimal bd = new BigDecimal("123.4567");
System.out.println(bd.multiply(bd)); // 15241.55677489
BigDecimal
用scale()
表示小数位数,例如:
BigDecimal d1 = new BigDecimal("123.45");
BigDecimal d2 = new BigDecimal("123.4500");
BigDecimal d3 = new BigDecimal("1234500");
System.out.println(d1.scale()); // 2,两位小数
System.out.println(d2.scale()); // 4
System.out.println(d3.scale()); // 0
可以对一个BigDecimal
设置它的scale
,如果精度比原始值低,那么按照指定的方法进行四舍五入或者直接截断:
public class HelloWorld {
public static void main(String[] args) {
BigDecimal d1 = new BigDecimal("123.456789");
BigDecimal d2 = d1.setScale(4, RoundingMode.HALF_UP); // 四舍五入,123.4568
BigDecimal d3 = d1.setScale(4, RoundingMode.DOWN); // 直接截断,123.4567
System.out.println(d2);
System.out.println(d3);
}
}
对BigDecimal
做加、减、乘时,精度不会丢失,但是做除法时,存在无法除尽的情况,这时,就必须指定精度以及如何进行截断:
BigDecimal d1 = new BigDecimal("123.456");
BigDecimal d2 = new BigDecimal("23.456789");
BigDecimal d3 = d1.divide(d2, 10, RoundingMode.HALF_UP); // 保留10位小数并四舍五入
BigDecimal d4 = d1.divide(d2); // 报错:ArithmeticException,因为除不尽
还可以对BigDecimal
做除法的同时求余数:
public class HelloWorld {
public static void main(String[] args) {
BigDecimal n = new BigDecimal("12.345");
BigDecimal m = new BigDecimal("0.12");
BigDecimal[] dr = n.divideAndRemainder(m);
System.out.println(dr[0]); // 102.0
System.out.println(dr[1]); // 0.105
System.out.println(Arrays.toString(dr)); // [102.0, 0.105]
}
}
调用divideAndRemainder()
方法时,返回的数组包含两个BigDecimal
,分别是商和余数,其中商总是整数,余数不会大于除数。我们可以利用这个方法判断两个BigDecimal
是否是整数倍数:
BigDecimal n = new BigDecimal("12.75");
BigDecimal m = new BigDecimal("0.15");
BigDecimal[] dr = n.divideAndRemainder(m);
if (dr[1].signum() == 0) {
// n是m的整数倍
比较BigDecimal
必须使用compareTo()
方法来比较,它根据两个值的大小分别返回负数、正数和0
,分别表示小于、大于和等于。
总是使用compareTo()比较两个BigDecimal的值,不要使用equals()!
BigDecimal d1 = new BigDecimal("123.456");
BigDecimal d2 = new BigDecimal("123.45600");
System.out.println(d1.equals(d2)); // false,因为scale不同
System.out.println(d1.equals(d2.stripTrailingZeros())); // true,因为d2去除尾部0后scale变为2
System.out.println(d1.compareTo(d2)); // 0
BigDecimal
也是从Number
继承的,也是不可变对象。
常用工具类
Math
求绝对值:
Math.abs(-100); // 100
Math.abs(-7.8); // 7.8
取最大或最小值:
Math.max(100, 99); // 100
Math.min(1.2, 2.3); // 1.2
计算xy次方:
Math.pow(2, 10); // 2的10次方=1024
计算√x:
Math.sqrt(2); // 1.414...
计算ex次方:
Math.exp(2); // 7.389...
计算以e为底的对数:
Math.log(4); // 1.386...
计算以10为底的对数:
Math.log10(100); // 2
三角函数:
Math.sin(3.14); // 0.00159...
Math.cos(3.14); // -0.9999...
Math.tan(3.14); // -0.0015...
Math.asin(1.0); // 1.57079...
Math.acos(1.0); // 0.0
Math还提供了几个数学常量:
double pi = Math.PI; // 3.14159...
double e = Math.E; // 2.7182818...
Math.sin(Math.PI / 6); // sin(π/6) = 0.5
生成一个随机数x,x的范围是0 <= x < 1
:
Math.random(); // 0.53907... 每次都不一样
如果我们要生成一个区间在[MIN, MAX)
的随机数,可以借助Math.random()
实现,计算如下:
public class HelloWorld {
public static void main(String[] args) {
double x = Math.random(); // x的范围是[0,1]
double min = 10;
double max = 50;
double y = x * (max - min) + min; // y的范围是[10,50]
long n = (long) y; // n的范围是[10,50]的整数
System.out.println(y); // 42.894399612728186
System.out.println(n); // 42
}
}
Random
Random r = new Random();
r.nextInt(); // 2071575453,每次都不一样
r.nextInt(10); // 5,生成一个[0,10)之间的int
r.nextLong(); // 8811649292570369305,每次都不一样
r.nextFloat(); // 0.54335...生成一个[0,1)之间的float
r.nextDouble(); // 0.3716...生成一个[0,1)之间的double
如果我们在创建Random
实例时指定一个种子,就会得到完全确定的随机数序列:
public class HelloWorld {
public static void main(String[] args) {
Random r = new Random(123456);
for (int i = 0; i < 10; i++) {
System.out.println(r.nextInt(100)); // 每次都一样
}
}
}
SecureRandom
有伪随机数,就有真随机数。实际上真正的真随机数只能通过量子力学原理来获取,而我们想要的是一个不可预测的安全的随机数,SecureRandom
就是用来创建安全的随机数的:
SecureRandom sr = new SecureRandom();
System.out.println(sr.nextInt(100));
SecureRandom
无法指定种子,它使用RNG(random number generator)算法。JDK的SecureRandom
实际上有多种不同的底层实现,有的使用安全随机种子加上伪随机数算法来产生安全的随机数,有的使用真正的随机数生成器。实际使用的时候,可以优先获取高强度的安全随机数生成器,如果没有提供,再使用普通等级的安全随机数生成器:
public class HelloWorld {
public static void main(String[] args) {
SecureRandom sr = null;
try {
sr = SecureRandom.getInstanceStrong(); // 优先获取高强度安全随机数生成器
} catch (NoSuchAlgorithmException e){
sr = new SecureRandom(); // 获取普通的安全随机数生成器
}
byte[] buffer = new byte[16];
sr.nextBytes(buffer); // 用安全随机数填充buffer
System.out.println(Arrays.toString(buffer)); // [87, 113, 1, -82, -6, -122, -121, 100, -82, -13, 73, 5, -53, 36, 89, 89]
}
}
异常处理
Java的异常
在计算机程序运行的过程中,总是会出现各种各样的错误。
有一些错误是用户造成的,比如,希望用户输入一个int
类型的年龄,但是用户的输入是abc
:
// 假设用户输入了abc:
String s = "abc";
int n = Integer.parseInt(s); // NumberFormatException!
程序想要读写某个文件的内容,但是用户已经把它删除了:
// 用户删除了该文件:
String t = readFile("C:\abc.txt"); // FileNotFoundException!
调用方如何获知调用失败的信息?有两种方法:
方法一:约定返回错误码。
如,处理一个文件,如果返回0
,表示成功,返回其他整数,表示约定的错误码:
int code = processFile("C:\test.txt");
if (code == 0) {
// ok:
} else {
// error:
switch (code) {
case 1:
// file not found:
case 2:
// no read permission:
default:
// unknown error:
}
}
方法二:在语言层面上提供一个异常处理机制。
try {
String s = processFile(“C:\test.txt”);
// ok:
} catch (FileNotFoundException e) {
// file not found:
} catch (SecurityException e) {
// no read permission:
} catch (IOException e) {
// io error:
} catch (Exception e) {
// other error:
}
捕获异常
捕获异常使用try…catch
语句,把可能发生异常的代码放到try {…}
中,然后使用catch
捕获对应的Exception
及其子类:
public class HelloWorld {
public static void main(String[] args) {
byte[] bs = toGBK("中文");
System.out.println(Arrays.toString(bs));
}
static byte[] toGBK(String s) {
try {
// 用指定编码转换String为byte[]
return s.getBytes("GBK");
} catch (UnsupportedEncodingException e) {
// 如果系统不支持GBK编码,会捕获到UnsupportedEncodingException:
System.out.println(e);
System.out.println("系统不支持GBK");
// 先记下来再说:
e.printStackTrace();
return s.getBytes();
}
}
}
如果是测试代码,上面的写法就略显麻烦。如果不想写任何try
代码,可以直接把main()
方法定义为throws Exception
:
public class HelloWorld {
public static void main(String[] args) throws Exception {
byte[] bs = toGBK("中文");
System.out.println(Arrays.toString(bs));
}
static byte[] toGBK(String s) throws UnsupportedEncodingException {
return s.getBytes("GBK");
}
}
Error
是无需捕获的严重错误,Exception
是应该捕获的可处理的错误;
RuntimeException
无需强制捕获,非RuntimeException
(Checked Exception)需强制捕获,或者用throws
声明;
捕获异常
在Java中,凡是可能抛出异常的语句,都可以用try … catch
捕获。把可能发生异常的语句放在try { … }
中,然后使用catch
捕获对应的Exception
及其子类。
finally语句
无论是否有异常发生,如果我们都希望执行一些语句
try {
process1();
process2();
process3();
} catch (UnsupportedEncodingException e) {
System.out.println("Bad encoding");
} catch (IOException e) {
System.out.println("IO error");
} finally {
System.out.println("END");
}
}
抛出异常
异常的传播
当某个方法抛出了异常时,如果当前方法没有捕获异常,异常就会被抛到上层调用方法,直到遇到某个try … catch
被捕获为止:
public class HelloWorld {
public static void main(String[] args) throws Exception {
try {
process1();
} catch (Exception e) {
e.printStackTrace();
}
}
static void process1(){
process2();
}
static void process2(){
Integer.parseInt(null); // 会抛出NumberFormatException
}
}
通过printStackTrace()
可以打印出方法的调用栈,类似:
java.lang.NumberFormatException: null
at java.base/java.lang.Integer.parseInt(Integer.java:614)
at java.base/java.lang.Integer.parseInt(Integer.java:770)
at Main.process2(Main.java:16)
at Main.process1(Main.java:12)
at Main.main(Main.java:5)
抛出异常
在catch
中抛出异常,不会影响finally
的执行。JVM会先执行finally
,然后抛出异常。
异常屏蔽
如果在执行finally
语句时抛出异常,那么,catch
语句的异常还能否继续抛出?例如:
这说明finally
抛出异常后,原来在catch
中准备抛出的异常就“消失”了,因为只能抛出一个异常。没有被抛出的异常称为“被屏蔽”的异常(Suppressed Exception)。
在极少数的情况下,我们需要获知所有的异常。如何保存所有的异常信息?方法是先用origin
变量保存原始异常,然后调用Throwable.addSuppressed()
,把原始异常添加进来,最后在finally
抛出:
大多数情况下,在finally
中不要抛出异常。因此,我们通常不需要关心Suppressed Exception
自定义异常
一个常见的做法是自定义一个BaseException
作为“根异常”,然后,派生出各种业务类型的异常。
BaseException
需要从一个适合的Exception
派生,通常建议从RuntimeException
派生:
public class BaseException extends RuntimeException {
}
其他业务类型的异常就可以从BaseException
派生:
public class UserNotFoundException extends BaseException {
}
public class LoginFailedException extends BaseException {
}
...
自定义的BaseException
应该提供多个构造方法:
public class BaseException extends RuntimeException {
public BaseException() {
super();
}
public BaseException(String message, Throwable cause) {
super(message, cause);
}
public BaseException(String message) {
super(message);
}
public BaseException(Throwable cause) {
super(cause);
}
}
上述构造方法实际上都是原样照抄RuntimeException
。这样,抛出异常的时候,就可以选择合适的构造方法。通过IDE可以根据父类快速生成子类的构造方法。
使用断言
断言(Assertion)是一种调试程序的方式。在Java中,使用assert
关键字来实现断言。
public static void main(String[] args) {
double x = Math.abs(-123.45);
assert x >= 0;
System.out.println(x);
}
语句assert x >= 0;
即为断言,断言条件x >= 0
预期为true
。如果计算结果为false
,则断言失败,抛出AssertionError
。
使用assert
语句时,还可以添加一个可选的断言消息:
assert x >= 0 : "x must >= 0";
这样,断言失败的时候,AssertionError
会带上消息x must >= 0
,更加便于调试。
Java断言的特点是:断言失败时会抛出AssertionError
,导致程序结束退出。因此,断言不能用于可恢复的程序错误,只应该用于开发和测试阶段。
实际开发中,很少使用断言。更好的方法是编写单元测试,后续我们会讲解JUnit
的使用。
使用JDK Logging
在编写程序的过程中,发现程序运行结果与预期不符,怎么办?当然是用System.out.println()
打印出执行过程中的某些变量,观察每一步的结果与代码逻辑是否符合,然后有针对性地修改代码。
代码改好了怎么办?当然是删除没有用的System.out.println()
语句了。
如果改代码又改出问题怎么办?再加上System.out.println()
。
反复这么搞几次,很快大家就发现使用System.out.println()
非常麻烦。
怎么办?
解决方法是使用日志
那什么是日志?日志就是Logging,它的目的是为了取代System.out.println()
。
因为Java标准库内置了日志包java.util.logging
,我们可以直接用。先看一个简单的例子:
public class HelloWorld {
public static void main(String[] args) throws Exception {
Logger logger = Logger.getGlobal();
logger.info("start process ...");
logger.warning("memory is running out ...");
logger.fine("ignord..");
logger.severe("process will be teminated ...");
}
}
运行上述代码,得到类似如下的输出:
6月 01, 2021 5:26:08 下午 com.java.demo1.HelloWorld main
信息: start process ...
6月 01, 2021 5:26:08 下午 com.java.demo1.HelloWorld main
警告: memory is running out ...
6月 01, 2021 5:26:08 下午 com.java.demo1.HelloWorld main
严重: process will be teminated ...
再仔细观察发现,4条日志,只打印了3条,logger.fine()
没有打印。这是因为,日志的输出可以设定级别。JDK的Logging定义了7个日志级别,从严重到普通:
- SEVERE
- WARNING
- INFO
- CONFIG
- FINE
- FINER
- FINEST
因为默认级别是INFO,因此,INFO级别以下的日志,不会被打印出来。使用日志级别的好处在于,调整级别,就可以屏蔽掉很多调试相关的日志输出。
因此,Java标准库内置的Logging使用并不是非常广泛。更方便的日志系统我们稍后介绍。
Commons Logging
使用Commons Logging
和Java标准库提供的日志不同,Commons Logging是一个第三方日志库,它是由Apache创建的日志模块。
Commons Logging的特色是,它可以挂接不同的日志系统,并通过配置文件指定挂接的日志系统。默认情况下,Commons Loggin自动搜索并使用Log4j(Log4j是另一个流行的日志系统),如果没有找到Log4j,再使用JDK Logging。
使用Commons Logging只需要和两个类打交道,并且只有两步:
第一步,通过LogFactory
获取Log
类的实例;第二步,使用Log
实例的方法打日志。
示例代码如下:
运行上述代码,肯定会得到编译错误,类似error: package org.apache.commons.logging does not exist
(找不到org.apache.commons.logging
这个包)。因为Commons Logging是一个第三方提供的库,所以,必须先把它下载下来。下载后,解压,找到commons-logging-1.2.jar
这个文件,再把Java源码Main.java
放到一个目录下,例如work
目录:
Commons Logging是使用最广泛的日志模块;
Commons Logging的API非常简单;
Commons Logging可以自动检测并使用其他日志模块。
Log4j
前面介绍了Commons Logging,可以作为“日志接口”来使用。而真正的“日志实现”可以使用Log4j。
当我们使用Log4j输出一条日志时,Log4j自动通过不同的Appender把同一条日志输出到不同的目的地。例如:
- console:输出到屏幕;
- file:输出到文件;
- socket:通过网络输出到远程计算机;
- jdbc:输出到数据库
以XML配置为例,使用Log4j的时候,我们把一个log4j2.xml
的文件放到classpath
下就可以让Log4j读取配置文件并按照我们的配置来输出日志。下面是一个配置文件的例子:
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
<Properties>
<!-- 定义日志格式 -->
<Property name="log.pattern">%d{MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36}%n%msg%n%n</Property>
<!-- 定义文件名变量 -->
<Property name="file.err.filename">log/err.log</Property>
<Property name="file.err.pattern">log/err.%i.log.gz</Property>
</Properties>
<!-- 定义Appender,即目的地 -->
<Appenders>
<!-- 定义输出到屏幕 -->
<Console name="console" target="SYSTEM_OUT">
<!-- 日志格式引用上面定义的log.pattern -->
<PatternLayout pattern="${log.pattern}" />
</Console>
<!-- 定义输出到文件,文件名引用上面定义的file.err.filename -->
<RollingFile name="err" bufferedIO="true" fileName="${file.err.filename}" filePattern="${file.err.pattern}">
<PatternLayout pattern="${log.pattern}" />
<Policies>
<!-- 根据文件大小自动切割日志 -->
<SizeBasedTriggeringPolicy size="1 MB" />
</Policies>
<!-- 保留最近10份 -->
<DefaultRolloverStrategy max="10" />
</RollingFile>
</Appenders>
<Loggers>
<Root level="info">
<!-- 对info级别的日志,输出到console -->
<AppenderRef ref="console" level="info" />
<!-- 对error级别的日志,输出到err,即上面定义的RollingFile -->
<AppenderRef ref="err" level="error" />
</Root>
</Loggers>
</Configuration>
反射
Class类
除了int
等基本类型外,Java的其他类型全部都是class
(包括interface
)。例如:
String
Object
Runnable
Exception
- …
仔细思考,我们可以得出结论:class
(包括interface
)的本质是数据类型(Type
)。无继承关系的数据类型无法赋值:
Number n =newDouble(123.456);// OK
String s =newDouble(123.456);// compile error!