JDK的安装

JDK和JRE

  • 什么是JRE?
    JRE(Java Runtime Environment)是java程序的运行环境,最核心的内容就是JVM(Java虚拟机)和核心类库。
  • 什么是JDK?
    JDK(Java Development Kit)是java的开发环境,包含了JRE,并提供了额外的开发者工具,例如javacjavaw等。

JDK的下载

推荐使用BellSoft的Liberica JDK。未来开发中可以根据需要选择不同的JDK版本。

配置JDK环境

  1. 打开设置-系统-系统信息-高级系统设置-环境变量。
  2. 在系统变量下新建变量名JAVA_HOME,变量值选择JDK目录。
  3. 打开系统变量下的Path变量,新建一个值,名为%JAVA_HOME%\bin,并建议将其置于前列。这样在我们有不同需要时,更改JAVA_HOME下的路径,即可切换不同的JDK版本。
    配置JDK环境
  4. 配置完成后,打开cmd输入java -version查看版本,能够查看到结果就说明已经成功安装并配置了JDK。
    查看Java版本

Java程序入门

第一个Java程序

1
2
3
4
5
6
public class HelloJava {  
public static void main(String[] args) {
System.out.println("Hello, Java!");
System.out.println("Hello World!");
}
}

我们将上述代码拆解:

  • java中的类,将在后续介绍。
    1
    2
    3
    public class HelloJava{

    }
  • java程序的入口点,称之为主方法。主方法中编写的代码将从上到下依次执行。
    1
    2
    3
    public static void main(String[] args) {

    }
  • java语法的代码。该代码和C语言中的printf()功能几乎相同。在java中,每一条完整的语句都需要以;结尾
    1
    2
    System.out.println("Hello, Java!");
    System.out.println("Hello World!");

注释

java中的注释分为三种:

  • 单行注释。
    1
    // 单行注释。
    通过//表示注释掉后面的内容,仅注释单行。
  • 多行注释。
    1
    2
    3
    4
    /*
    这是多行注释。
    可以表示多行文本内容。
    */
    通过/* */表示,其中间的每一行都会被注释。
  • 文档注释。
    1
    2
    3
    4
    5
    6
    /**  
    * 这是文档注释。
    * main方法是程序的入口。
    * @author Soria
    * @param args 命令行参数
    */
    文档注释在多行注释的基础上,允许在程序中嵌入关于程序的信息,可以使用额外的参数生成文档,例如上述的@author@param。具体查看【[[#文档注释]]】。
    文档注释

变量和常量

常量

常量使用final修饰,具体为final 数据类型 标识符。常量只有第一次赋值时可以更改它的值,后续将不能修改。

1
2
3
final int a = 10;
final int b;
b = 10;

变量

变量的值可以修改,具体为数据类型 标识符

1
2
3
4
5
int a = 20;
a = 30;

int b = 30;
int c = b;

标识符的命名要求

  • 标识符可以由大小写字母、数字、下划线_和美元符号$组成,但是不能以数字开头。
  • 变量不能重复定义,大小写敏感,A和a就是两个不同的变量。
  • 不能有 @#+-/等符号。
  • 应该使用有意义的名称,一般我们采用英文单词,以小写字母开头。
  • 不可以是truefalse
  • 不能与java语言的关键字或是基本数据类型重名。

基本数据类型

整数类型

java中的整数类型包含下面几种:

类型 取值范围
byte -128~127
short -32768~32767
int -231~231-1
long -263~263-1
上述变量都可以表示整数,且由于大小关系,可以将小整数类型传递给大整数类型。
1
2
short a = 120;
int b = a;

手动赋值时的数值默认是int类型,在表达long类型的变量时,需要在结尾添加大写或小写的L。如果数值过大,我们可以用_分隔,便于提升可阅读性。

1
2
long a = 922337203685477580L;
int b = 1_000_000;

在java中还可以通过不同的进制来表示常量或变量值。

1
2
3
int a = 0b10101010;    // 二进制
int b = 01122; // 八进制
int c = 0x1A2B3C; // 十六进制

浮点类型

浮点类型包含单精度浮点型float和双精度浮点型double
浮点类型的取值范围如下:

  • float:1.4*10-45~3.4028235*1038
  • double:4.9*10-324~1.7976931348623157*10308
    创建浮点类型时,默认为double类型,在表达float类型的变量时,需要在结尾添加大写或小写的F
    1
    2
    float f = 9.9F;
    double d = 12.55;

字符类型

java中的字符类型为char,其存储的内容依旧是数字,范围为0~65535,每个数字都对对应着一个字符,具体需查看ASCII码表
我们既可以用ASCII码表所对应的数字表示字符,也可以直接将字符常量赋值。两种写法的效果没有区别。

1
2
3
4
char c1 = 65;  
char c2 = 'A';
System.out.println(c1); // 输出:A
System.out.println(c2); // 输出:A

char实际上需要两个字节才能表示更多种类的字符,因此可以表示中文字符。使用int类型接收字符类型常量值可以直接转换为对应的编码

1
2
3
4
int a = '你';  
int b = '好';
System.out.println(a); // 输出:20320
System.out.println(b); // 输出:22909

如果遇到多个字符,我们需要使用String类型表示。String类型并不是基本数据类型,而是对象类型,将在之后介绍。

1
String str = "你好,世界";

布尔类型

布尔类型常用于存放状态,只有truefalse两种值,表示真和假,常用于流程控制语句。

1
2
boolean a = true;
boolean b = false;

隐式转换

1
2
3
long l = 21731371236768L;
float f = l;
System.out.println(f);

隐式转换是指在不需要显式声明的情况下,自动将一种较小的类型转换为较大的兼容类型。当在表达式中混合使用不同类型的变量时,会自动将较小类型提升为较大类型进行运算。

1
2
3
4
int a = 10;
double d = 5.5;
double result = a + d; // int自动转换为double
System.out.println(result); // 输出15.5

当涉及到 byteshortchar类型的运算时,这些类型会自动提升为int,即使两者的值都很小。这是为了确保计算的准确性和避免可能的溢出问题。

1
2
3
4
5
6
7
8
9
byte b = 10;
short s = 20;
char c = 'A'; // 'A' 的 Unicode 值是 65

int result1 = b + s; // byte 和 short 自动转换为 int
int result2 = b + c; // byte 和 char 自动转换为 int

System.out.println(result1); // 输出 30
System.out.println(result2); // 输出 75 (10 + 65)

隐式类型转换规则:byteshort(char)intlongfloatdouble

显式转换

当需要将较大的数据类型转换为较小的数据类型时,必须手动完成。

1
2
3
double d = 9.99;
int i = (int) d; // 强制将 double 转换为 int
System.out.println(i); // 输出 9

数据丢失是显示转换的常见问题

运算符

赋值运算符

赋值运算符:=。 其左边必须是一个可以赋值的目标,比如变量,右边可以是任意满足要求的值,包括变量。
当出现连续使用赋值运算符时,按照从右往左的顺序进行计算,b被赋值为666。赋值运算的计算结果就是赋的值本身,此时继续进行赋值计算,c将被赋值为b = 666的计算结果,所以c的值也为666

1
2
3
int a = 666;    // 对变量a进行赋值
int b;
int c = b = 666;

算数运算符

算数运算符:+(加法)、-(减法)、*(乘法)、/(除法)、%(取余)。这些运算符均具有参与基本数据类型计算的能力。值得注意的是,两个整数在进行/运算时,得到的结果会舍去小数部分,保留整数。

1
2
3
4
int a = 10;
int b = a + 10; // b = 20,直接进行相加
int c = a / 3; // c = 3,结果保留整数
int d = a % 6; // d = 2,10 % 6 = 1余4,结果为4

+还支持字符串与字符串的拼接,以及和基本数据类型的拼接。

1
2
String str1 = "测试内容:" + "拼接字符串";
String str2 = "测试内容" + true + 10.5 + 'A';

括号运算符

和数学运算符的括号相同,可以使用()提升内部运算的优先级。

1
2
3
4
5
6
7
8
9
10
int a = 10;
int b = (a = 8) * (-a + 10);
/*
* 首先计算a = 8,此时a被赋值为8
* 然后计算-a,得到-a = -8
* 再计算-a + 10,得到结果2
* 最后再计算相乘,得到10
* 所以最后会输出16
*/
System.out.println(b);

括号也可以嵌套,但不像数学中使用[]{},统一使用()

1
2
3
4
5
6
7
int a = (1+(2 - 3) * 4) * 5;
/*
* 先计算内部(2 - 3),然后计算相乘,得到-4
* 再计算和1相加,得到-3
* 最后计算和5相乘,得到-15
*/
System.out.println(a);

()还用作显式转换。

1
2
int a = 10;
short b = (short) a;

通过显式转换,可以将两个整数相除得到的结果变为小数。

1
2
3
int a = 10, b = 3;
// 将其中一个变量显式转换为double类型,根据隐式转换,都会变成double类型参与计算
double c = (double) a / b;

自增自减运算符

自增自减运算符一般和变量进行使用。

1
2
3
4
5
6
int a = 5;
a++;
int b = 10;
b--;
System.out.println(a); // 输出6
System.out.println(b); // 输出9

自增自减运算符还可以位于变量前面,例如++a--b,效果和上述相同。但是两种方式的计算顺序不一样。

1
2
3
4
5
int a = 10;
System.out.println(a++); // 输出10
System.out.println(a); // 输出11
int b = 10;
System.out.println(++b); // 输出11

当自增自减运算符在变量后面时,先计算结果在自增自减。反之在前面时,则相反。
自增自减运算符还可以用于其他数的自增自减,而不是1。

1
2
3
4
5
6
int a = 10;
a += 4; // 等效 a = a + 4;
int b = 10;
b *= 3; // 等效b = b * 3;
System.out.println(a); // 输出14
System.out.println(b); // 输出30

算数运算符+-*/都能和自增自减运算符相组合。

位运算符

位运算符比较偏向底层,包括四种运算符:&|^~

  • &:按位与,两个数的每一位相比较,相同则为1,不同则为0;
  • |:按位或,两个数的每一位相比较,其中一个为1则为1,两个都为0则为0;
  • ^:按位异或,两个数的每一位相比较,不同则为1,相同则为0;
  • ~:按位取反,对一个数的每一位取反,原来为1则为0,原来为0则为1;
    1
    2
    3
    4
    5
    int a = 3, b = 6;    // a = 0011, b = 0110;
    System.out.println(a & b); // 输出:2 -> 0010
    System.out.println(a | b); // 输出:7 -> 0111
    System.out.println(a ^ b); // 输出:5 -> 0101
    System.out.println(~a); // 输出:-4 -> 1100
    除了上述四个位运算符,还有位移运算符:>><<
    左移运算符>>每移动一位,结果会*2,高位丢弃,低位补0。右移运算符<<每移动一位,结果都会/2,低位丢弃,高位补符号位。
    1
    2
    3
    4
    5
    int a = 3, b = 6;
    System.out.println(a << 1); // 输出:6
    System.out.println(b >> 1); // 输出:3
    // 位移运算符还可以和 = 一起使用
    b <<= 1;

关系运算符

关系运算符包括:><>=<=!===。常用于判断,输出的结果为boolean类型的truefalse

1
2
3
4
5
6
System.out.println(3 > 5);    // 输出:false
System.out.println(3 >= 5); // 输出:false
System.out.println(3 == 3); // 输出:true
System.out.println(3 != 5); // 输出:true
System.out.println(3 <= 5); // 输出:true
System.out.println(5 <= 5); // 输出:true

逻辑运算符

逻辑运算符有三种:&&||

  • &&:两边同时为true,则返回true,否则返回false
  • ||:两边有一个为ture,则返回true,否则返回false
  • !:对运算结果取反,结果为true则返回false,结果为false则返回true
    1
    2
    3
    4
    5
    6
    System.out.println(3 > 2 && 3 > 3);    // 输出:false
    System.out.println(3 > 1 && 3 > 2); // 输出:true
    System.out.println(3 < 4 || 3 < 3); // 输出:true
    System.out.println(3 > 3 || 3 < 3) // 输出:false
    Sytem.out.println(!(3 > 3)); // 输出:true
    System.out.println(!(3 > 2)) // 输出:false

三元运算符

三元运算符的形式为判断语句?结果1:结果2。如果判断语句结果为true,返回结果1,反之则返回结果2

1
2
3
int a = 10;
System.out.println(a > 5 ? 'A' : 'B'); // 输出:A
System.out.println(a < 5 ? 'A' : 'B'); // 输出:B

流程控制语句

代码块和作用域

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
int a = 10; //此时变量在最外层定义
{
int b = 20;
System.out.println(a); // 处于其作用域内部的代码块可以使用
System.out.println(b); // 也可以使用
}
System.out.println(a); // 这里肯定也可以使用
System.out.println(b); // 此处无法使用
}

变量的使用范围,仅限于其定义时所处的代码块,一旦超出对应的代码块区域,那么就相当于没有这个变量。

选择结构

if语句

if语句表示为:if( 判断条件 ) 判断成功执行的代码块;

1
2
3
4
if( 15 > 10) {
// 将会执行此处的代码块
System.out.println("15 > 10");
}

我们还可以通过elseelse-if来执行其他情况的代码块。

1
2
3
4
5
6
7
8
if(15 > 10) {
System.out.println("15 > 10");
} else if (15 == 10) {
System.out.println("15 = 10");
} else {
// 将会执行此处的代码块
System.out.println("15 < 10");
}

switch语句

switch语句可以精准的匹配某个值,但不能判断范围。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int a = 10;
// switch的第一种写法
switch (a) {
case 10 -> System.out.println("a is 10");
case 20 -> System.out.println("a is 20");
default -> System.out.println("a is not 10 or 20");
}
// switch的第二种写法
switch (a) {
case 10:
System.out.println("a is 10");
break;
case 20:
System.out.println("a is 20");
break;
default:
System.out.println("a is not 10 or 20");
}

循环结构

while循环

while循环的语法为:while (循环条件) 循环体

1
2
3
4
5
int i = 100;
while(i > 0){
System.out.println(i);
i /= 2;
}

当我们不确定何时结束循环时,适合使用while循环。
我们也可以先执行一次循环体,再做循环判断,需要用到do...while语句。

1
2
3
4
5
int i = 0;
do {
System.out.println("Hello World");
i++;
} while (i < 10);

此时,无论满不满足循环条件,都会先执行一次do代码块内的内容。

for循环

for循环的语法为:for (表达式1; 表达式2; 表达式3) 循环体

  • 表达式1:循环开始前仅执行一次。
  • 表达式2:判断语句,用于判断是否可以结束循环。
  • 表达式3:每次循环结束后都会执行一次。
    1
    2
    3
    4
    // 下面的循环会执行3次,每次都会输出一次i的值
    for (int i = 0; i < 3; i++) {
    System.out.println(i);
    }
    for循环的三个表达式都不是必需项,可以省略不写。其中,表达式2省略不写会造成无限循环
    1
    2
    3
    4
    5
    int i = 0;
    for ( ; i < 3; ) {
    System.out.println(i);
    i++;
    }
    我们还可以使用continuebreak来提前结束循环。
  • continue:跳过本轮循环,直接开始下一轮循环。
  • break:终止循环,跳出到循环外部。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    for (int i = 0; i < 3; i++) {
    if (i == 1) continue; // 当i=1时,跳过后续代码,并执行下一轮循环
    System.out.println(i);
    }

    for (int i = 0; i < 3; i++) {
    if (i == 0) break; // 当i=1时直接结束循环,并跳出循环,执行输出Hello
    System.out.println(i);
    }
    System.out.println("Hello");

Java程序基础

类与对象

每个.java源文件只能有一个public class

1
2
3
4
5
6
7
8
9
10
11
public class Person {
// 直接在类中定义变量,表示类具有的属性
String name;
int age;
String sex;
}

class Student {
...
...
}

通过new关键字可以创建一个对象。

1
2
Person p = new Person();
Student s = new Student();

方法

方法的创建与使用

方法的定义:返回值类型 方法名(参数) { 方法体 }

1
2
3
4
5
6
7
8
public class Person {
String name;
int age;

public void hello() {
System.out.println("我叫" + name ",今年" + age + "了。");
}
}

返回值类型是指方法执行完成后所返回的数据类型,可以是基本类型也可以是引用类型。如果方法没有返回值,可以用void表示。

1
Person p = new Person();

在需要调用方法时,使用.运算符即可。

1
2
3
4
Person p = new Person();
p.name = "小明";
p.age = 18;
p.hello(); // 输出:我叫小明,今年18岁了。

方法定义时还可以使用参数。下面我们定义一个加法:

1
2
3
int sum(int a, int b) {
return a + b;
}

使用return关键字后,方法会直接结束并返回结果,后续代码无法到达。

方法的参数传递

方法定义时创建的参数称为形式参数,简称形参。调用方法实际传入的参数称为实际参数,简称实参。
在方法的参数传递时,会在调用方法的时候,对参数的值进行复制,而不是使用传入参数本身。

1
2
3
4
5
6
7
8
9
10
int a =3, b = 6;
swap(a, b);
System.out.println("a = " + a + ", b = " + b); // 输出:a = 3, b = 6

// 交换a和b的值
void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
}

上述方法中,交换的仅仅是swap方法里的ab,并不能直接操作外面的数。
当我们操作对象时,情况会有所不同。

1
2
3
4
5
6
7
8
void changeName(Person p, String name) {
p.name = name;
}

Pseron p = new Person();
p.name = "小明";
p.changeName(p, "小华");
System.out.println(p.name); // 输出:小华

此处引用类型p是对象的引用,而不是对象本身。这里进行的值传递相当于将对象的引用复制到方法内部的变量中,而这个内部的变量引用的依旧是同一个对象。所以此处相当于直接操作外面所定义的对象。

this关键字的使用

如果我们想要在方法中访问当前对象的属性,可以使用this关键字。

1
2
3
4
5
6
7
8
9
10
void getName() {
return name;
}
void setName(String name) {
this.name = name;
}

Person p = new Person;
p.setName("小明");
System.out.println(p.getName());

方法的重载

我们现在有一个加法:

1
2
3
int sum(int a, int b) {
return a + b;
}

该方法只接受int类型的变量。如果我们传入小数,将会出现错误。此时,为了让上述方法支持小数的相加,我们需要对其进行重载。

1
2
3
4
5
6
7
8
9
10
11
int sum(int a, int b) {
return a + b;
}

double sum(double a, double b) {
return a + b;
}

int sum(int a, int b, int c) {
return a + b + c;
}

方法的重载意味着方法可以重名,但是传入的参数类型不同,又或者是参数的数量不同。返回值既可以相同也可以不同。
如果仅仅是返回值不同,是不支持重载的。

方法的递归调用

方法可以调用其他方法或者自身。方法如果自己调用自己,称之为递归调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void sayHello() {
System.out.println("Hello");
}

// 方法调用其他方法
void useSay() {
sayHello();
}

// 方法的递归调用
int fibonacci(int n) {
if( n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}

构造方法

构造方法不需要填写返回值,并且方法名称与类名相同,默认情况下每个类都会自带一个没有任何参数的无参构造方法(只是不用我们去写,编译出来就自带)当然,我们也可以手动声明,对其进行修改。

1
2
3
4
5
6
7
8
9
10
11
12
public class Person{
String name;
int age;
// 构造方法
public Person() {
name = "小明";
age = 18;
}
}

Person p = new Person();
System.out.println(p.name); // 输出:小明

构造方法也支持重载,但在实例化时需要使用我们重载的构造方法,又或者是手动重载一个默认无参构造方法。

1
2
3
4
5
6
public Person(String name, int age) {
this.name = name;
this.age = age;
}

Person p = new Person("小明", 18);

静态变量和静态方法

静态的内容,我们可以理解为是属于这个类的,也可以理解为是所有对象共享的内容。我们通过使用static关键字来声明一个变量或一个方法为静态的,一旦被声明为静态,那么通过这个类创建的所有对象,操作的都是同一个目标,也就是说,对象再多,也只有这一个静态的变量或方法。一个对象改变了静态变量的值,那么其他的对象读取的就是被改变的值。

1
2
3
4
5
6
7
8
9
10
public class Person() {
String name;
int age;
static String info;
}

Person p1 = new Person();
p1.info = "你好";
Person p2 = new Person();
System.out.println(p2.info); // 输出:你好

静态方法同样是属于类的,而不是具体的某个对象。

1
2
3
4
static String info;
static void printInfo() {
System.out.println(info);
}

代码块也可以是静态的。

1
2
3
4
static String info;
static {
info = "信息";
}

所有被标记为静态的内容,会在类刚加载的时候就分配,而不是在对象创建的时候分配,所以说静态内容一定会在第一个对象初始化之前完成加载。

包和访问控制

包的声明和导入

包用来区分类的位置。随着我们的程序不断变大,可能会创建各种各样的类,他们可能会做不同的事情,那么这些类如果都放在一起的话,有点混乱,我们可以通过包的形式将这些类进行分类存放。
包的命名规则同样是英文和数字的组合。

  • 个体项目(individual),指个人发起,但非自己独自完成的项目,可公开或私有项目,copyright主要属于发起者。 包名为indi.发起者名.项目名.模块名.…
  • 单人项目(one-man),指个人发起,但非自己独自完成的项目,可公开或私有项目,copyright主要属于发起者。 包名为onem.发起者名.项目名.模块名.…
  • 个人项目(personal),指个人发起,独自完成,可分享的项目,copyright主要属于个人。包名为pers.个人名.项目名.模块名.…
  • 私有项目(private),指个人发起,独自完成,非公开的私人使用的项目,copyright属于个人。 包名为priv.个人名.项目名.模块名.…
  • 团队项目(team),指由团队发起,并由该团队开发的项目,copyright属于该团队所有。包名为team.团队名.项目名.模块名.…
  • 公司项目(company),copyright由项目发起的公司所有。包名为com.公司名.项目名.模块名.…
    通过package关键字,我们可以指明当前类所属的包。当我们需要使用其他包里的类时,可以通过import关键字导入。
    在不同包下的两个类,即使类名相同,也是不同的类。
    1
    2
    3
    4
    5
    6
    7
    package com.test;
    import com.project.entity.Student;

    public class Person {
    String name;
    int age;
    }

访问权限控制

我们可以为成员变量、成员方法、静态变量、静态方法甚至是类指定访问权限,不同的访问权限,有着不同程度的访问限制。

  • public - 公共,标记为公共的内容,允许在任何地方被访问。
  • private - 私有,标记为私有的内容无法被除当前类以外的任何位置访问。
  • protected - 受保护,标记为受保护的内容可以能被类本身和同包中的其他类访问,也可以被子类访问。
  • 什么都不写 - 默认,默认情况下,只能被类本身和同包中的其他类访问。
访问权限 当前类 同一个包下的类 不同包下的类 不同包下的子类
public
private
protected
默认

封装、继承和多态

封装、继承和多态是面向对象编程的三大特性。

  • 封装,把对象的属性和方法结合成一个独立的整体,隐藏实现细节,并提供对外访问的接口。
  • 继承,从已知的一个类中派生出一个新的类,叫子类。子类实现了父类所有非私有化的属性和方法,并根据实际需求扩展出新的行为。
  • 多态,多个不同的对象对同一消息作出响应,同一消息根据不同的对象而采用各种不同的方法。

类的封装

封装的目的是为了保证变量的安全性,使用者不必在意具体实现细节,而只是通过外部接口即可访问类的成员,如果不进行封装,类中的实例变量可以直接查看和修改,可能给整个代码带来不好的影响,因此在编写类时一般将成员变量私有化,外部类需要使用gettersetter方法来查看和设置变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public String setName(String name) {
this.name = name;
}
...

我们还可以将构造方法变为private,通过我们自己定义的方法来构造对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Person {
String name;
int age;

private static Person p;

private Person() {}

public static Person getInstance() {
if(p == null)
p = new Person();
return p;
}
}

类的继承

使用extends关键字,可以让一个类继承另一个类。父类中所定义的方法和属性都会被子类继承,子类可以直接使用父类中的属性和方法。

1
2
3
4
5
6
7
8
9
10
public class Person {
String name;
int age;
}

class Student extends Person {
public void study() {
System.out.println("我叫" + name + ", 我正在学习");
}
}

类的继承可以不断向下,但是同时只能继承一个类。对于父类中被private修饰的属性,子类依旧继承了这个属性,但是无法访问。final修饰的类不允许被继承。
我们还可以使用强制类型转换,将子类当作其父类使用,也可以将一个被当作父类使用的子类对象转换回子类。

1
2
Person p = new Student("小明", 18);    // 向上转型
Student s = (Student) p; // 向下转型vc

instanceof可以判断某个变量所引用的对象是什么类。

1
2
3
4
5
6
7
Person p = new Student("小明", 18);
if(p instanceof Student) {
System.out.println("我属于Student类");
}
if(p instanceof Person) {
System.out.println("我属于Person类");
}

子类可以拥有和父类同名的变量或方法,通过super关键字可以使用父类中同名的属性和变量。

1
2
3
4
5
6
7
8
9
10
11
public class Person {
public void hello() {
System.out.println("hello");
}
}

public class Student extends Person {
public void hello() {
super.hello();
}
}

方法的重写

使用@override注解,我们可以对方法进行重写。

1
2
3
4
5
6
7
8
9
10
11
12
public class Person {
...
@Override
public boolean equals(Object obj) {\
if(obj == null) return false;
if(obj instanceof Person) {
Person p = (Person) obj;
return this.name.equals(p.name) && this.age.equals(p.age);
}
return false;
}
}

在重写了Object类提供的equals方法后,就会按照我们重写后的方法进行判断,即使是两个不同的对象。注意,被finalprivate修饰的类无法重写。
Object类是所有类的父类,所有类都继承自Object类。

抽象类

抽象类不是具体的类定义,不可以被new关键字实例化对象。抽象类相当于定义,具体的实现需要通过子类去完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public abstract class Person {
protected String name;
protected int age;

protected Person(String name, int age) {
this.name = name;
this.age = age;
}
// 只有对抽象方法的定义,没有具体实现
public abstract void exam();
}

public class Student extends Person {
public Student(String name, int age) {
super(name, age);
}

@override
public void exam() {
System.out.println("我是学生,考试满分。");
}
}

抽象类一般只用作继承,抽象类的子类也可以是一个抽象类。

接口

接口用于将类所具有的行为抽象出来。接口不同于继承,接口可以同时实现多个。接口中不允许存在成员变量和成员方法,但是可以存在静态变量和静态方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public interface Study {
final int a = 10;
void study();
// 通过default关键字定义接口中方法的默认实现
default void write() {
System.out.println("写");
}
}

class Student extends Person implements Study {
public Student(String name, int age) {
super(name, age);
}

@override
public void study() {
System.out.println("学习");
}
}

class Teacher extends Person implements Study, A, B, ... {
...
}

枚举类

使用enum定义枚举类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public enum Status {
WALKING("走路"), RUNNING("跑"), STOPPING("停止");

private final String name;
Status(String name) {
this.name = name;
}
public String getName() {
return name;
}
}

public class Person {
String name;
int age;
Status status;

public Status getStatus() {
return status;
}

public void setStatus(Status status) {
this.status = status;
}
}

Person p = new Person("小明", 18);
p.setStatus(Status.RUNNING);
System.out.println(p.getStatus().getName()); // 输出:跑

Java程序高级

包装类

基本类型包装类

能够表示数字的基本类型包装类,继承自Number类,对应关系如下表:

基本类型 包装类
byte Byte
boolean Boolean
short Short
char Character
int Integer
long Long
float Float
double Double
包装类型具有自动装箱和拆箱机制,可以更好地参与到基本运算中。
1
2
Integer a = 10, b = 20;    // 自动装箱为包装类
int c = a * b; // 自动拆箱为基本类型参与运算

特殊包装类

特殊包装类分为用于进行超大数计算的BigInteger和小数精确计算的BigDecimal

1
2
3
4
5
6
7
8
9
10
11
12
public class HelloJava {  
public static void main(String[] args) {
// 超大数计算
BigInteger bigI = BigInteger.valueOf(Long.MAX_VALUE);
bigI = bigI.pow(50); // long最大值的50次方
System.out.println(bigI);

BigDecimal bigD = BigDecimal.valueOf(Double.MAX_VALUE);
bigD = bigD.divide(BigDecimal.valueOf(3),100, RoundingMode.CEILING); // double最大值除以3并保留100位小数
System.out.println(bigD);
}
}

BigInteger和BigDecimal

数组

数组用于存放一组相同类型的数据,每一个数据称之为数组的元素。数组的下标从0开始,要访问数组内的元素可以通过下标访问。

1
2
3
4
5
6
// 创建数组并赋值的两种方式
int[] array = new int[10]{1, 2, 3, ...};
int[] array2 = new int[15];
array2[0] = 15;

System.out.println(array[0]); // 输出:1

创建出来的数组每个位置上都有默认值,如果是引用类型,就是null,如果是基本数据类型,就是0,或者是false,跟对象成员变量的默认值是一样的。
数组的长度length在创建后就被确定,且被final修饰。因此数组一旦创建,长度便不可被修改,要使用更大或更小的数组,只能重新创建。

1
2
int[] array = new int[10];
System.out.println(array.length);

多维数组

数组中也可以存放数组类型的数据,例如二维数组:

1
2
3
4
// 数组创建时长度必须被确定。但内层相当于外层数组的一个元素,因此可以不需要确定长度
int[][] array = new int[10][];
array[0] = new int[] {1, 2, 3};
array[1] = new int[] {4, 5, 6, 7};

在遍历多维数组时,我们需要嵌套循环。

1
2
3
4
5
6
int[][] array = new int[2][3] {{1, 2, 3}, {4, 5, 6}};
for(int i = 0; i < 2; i++) {
for(int j = 0; j < 3; j++) {
System.out.println(array[i][j]);
}
}

可变长参数

方法支持可变长参数,即接收任意数量参数的方法。可变长参数的的本质就是一个数组。

1
2
3
4
5
6
public void test(String... strings){ 
//strings这个变量就是一个String[]类型的数组
for (String string : strings) {
System.out.println(string);
}
}

如果同时存在多个参数,那么可变长参数只能放在最后。

1
2
3
public void test(int a, int b, int... arrays) {
...
}

String

String

String本身也是一个类,只不过它比较特殊,每个用双引号括起来的字符串,都是String类型的一个实例对象。

1
2
String str = "Hello World";
String str2 = new String("Hello World too");

如果是直接使用双引号创建的字符串,如果它们内容相同,为了优化效率,那么始终都是同一个对象。但是如果我们使用构造方法主动创建两个新的对象,那么就是不同的对象了。

1
2
3
4
5
6
7
String a = "hello";
String b = "hello";
System.out.println(a == b); // 输出:true

String c = new String("hello");
String d = new String("hello");
System.out.println(c == d); // 输出:false

如果我们仅仅是想要判断两个字符串的内容是否相同,不要使用==String类重载了equals方法用于判断和比较内容是否相同。

1
System.out.println(c.equals(d));    // 输出:true

正则表达式

正则表达式用于解决字符串格式匹配问题。具体查看【[[#正则表达式匹配词]]】。

1
2
3
String str = "abcabccaa";
// 表示abc这几个字符可以出现n次
System.out.println(str.matches("[abc]*"));

内部类

成员内部类

我们可以在类的内部在定义一个类。

1
2
3
4
5
6
7
public class Outer {
public class Inner{
public void sayInner() {
System.out.println("我是内部类");
}
}
}

如果将内部类的访问权限改为private,那么外部则无法访问到内部类。成员内部类和成员方法、成员变量一样,是对象所有的,而不是类所有的。

1
2
3
4
Outer out = new Outer();
out.Inner in = out.new Inner();
// 实例化后我们也可以使用内部类的方法
in.sayInner();

成员内部类可以访问到外部类,也可以通过thissuper来使用同名变量,我们一般不实例化成员内部类,只会在类的内部自己使用。

静态内部类

静态内部类就像静态方法和静态变量一样,是属于类的,我们可以直接创建使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Outer {
private final String name;

public static class Inner {
public void say() {
System.out.println("我是静态内部类");
}
}
}

...

Outer.Inner in = Outer.new Inner();
in.say();

静态内部类相当于外部而言,整个内部类处于静态上下文,因此无法访问外部类的非静态属性。

局部内部类

局部内部类使用频率很低,基本用不到。
局部内部类就像局部变量一样,可以在方法中定义。

1
2
3
4
5
6
7
8
9
10
11
public class Outer {
public void say() {
class Inner {
public void say() {
System.out.println("我是局部内部类");
}
}
Inner in = new Inner();
in.say();
}
}

匿名内部类

匿名内部类一般用于抽象类和接口。一般情况下抽象类和接口需要继承或实现,然后再重写内部的方法。我们可以在方法中使用匿名内部类,将其中的抽象方法实现,并直接创建实例对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
public abstract class Person {
public abstract void say();
}

...

Person p = new Person() {
@override
public void say() {
System.out.println("匿名内部类");
}
}
p.say();

异常

异常的类型

所有的异常都继承自Exception类。 异常主要分为运行时异常和编译时异常两大类。运行时异常默认继承自RuntimeException类,所有的编译时异常默认继承自Exception类。

自定义异常

要创建自定义异常,只需要继承Exception类或者RuntimeException类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 编译时异常
public class MyException extends Exception {
// message就是造成异常的原因
public MyException(String message) {
super(message);
}
}

// 运行时异常
public class MyRuntimeException extends RuntimeException {
public MyRuntimeException(String message) {
super(message);
}
}

抛出异常

由于一些原因导致程序执行错误时,例如传入错误的参数,我们可以使用throw关键字手动抛出一个异常,并在异常的构造方法中写入一个信息来表示造成异常的原因。

1
2
3
4
5
public double divide(int a, int b) {
if( b == 0)
throw new RuntimeException("被除数不能为0");
return a / b;
}

当出现异常时,程序会终止并报错,并显示出现异常的位置。
抛出异常
如果我们在方法中抛出了一个非运行时异常,那么必须告知函数的调用方我们会抛出某个异常,函数调用方必须要对抛出的这个异常进行对应的处理。运行时异常也可以这样写,但不做强制要求。

1
2
3
4
//告知调用方此方法会抛出哪些异常,请调用方处理好 
private static void test() throws Exception {
throw new Exception("我是编译时异常!");
}

异常的处理

我们一般使用try...catch捕获并处理异常。将代码写在try语句块中,catch可以捕获到try中发生的特定异常并进行处理。

1
2
3
4
5
6
7
try {
String str = null;
str.toString();
} cathc(NullPointerException e) {
// 特定处理
System.out.println("异常信息:" + e.getMessage());
}

当要捕获的异常是某个异常的子类时,我们对其父类进行捕获同样可以包括到子类。例如:可以直接捕获Exception类来对所有异常进行捕获,因为Exception类是所有异常的父类。

1
2
3
4
5
try {
int a = 10 / 0; // ArithmeticException
} catch(Exception e) { // 可以直接通过Exception捕获到
...
}

如果我们希望无论发生异常都会执行后续的代码,可以使用try...catch...finally。在finally语句块中的代码,无论是否捕获到异常都会执行。

1
2
3
4
5
6
7
8
try {
...
} catch( ... ) {
...
} finally {
// 无论是否捕获到异常,都会执行
System.out.println("finally");
}

try必须搭配catchfinally使用。省略掉catch后则不捕获异常,但finally语句块依旧会执行。

断言

断言一般用于测试,正常程序中不会使用。使用assert关键字可以设置断言,如果断言后面的内容为false,则抛出AssertionException异常。

1
2
int a = 10;
assert a > 10: "我是断言异常";

断言

Lambda表达式

扩展内容

文档注释

标签 描述 示例
@author 标识一个类的作者 @author Soria
@deprecated 指定一个过期的类或成员 @deprecated 这是一个过时的方法,不推荐使用。
{@docRoot} 指明当前文档根目录的路径 {@docRoot} 路径
@exception 标志一个类抛出的异常 @exception IOException 异常处理
{@inheritDoc} 从直接父类继承的注释 {@inheritDoc} 从父类继承的注释
{@link} 插入一个到另一个主题的链接 {@link #main(String[] args)}方法是程序的入口。
{@linkplain} 插入一个到另一个主题的链接,但该链接显示纯文本字体 和上述相同。
@param 说明一个方法的参数 @param a 一个参数
@ImplSpec 实现要求 @ImplSpec 这是实现要求
@ImplNote 实现说明 @ImplNote 这是实现说明
@return 说明返回值类型 @return 返回结果
@see 插入一个类到另一个类的链接 @see java.lang.String
@serial 说明一个序列化属性 @serial 描述文本
@serialData 说明通过writeObject()writeExternal()方法写的数据 @serialData描述文本
@serialField 说明一个ObjectStreamField组件 @serialField name 姓名
@since 自何时开始 @since release
@throws @exception相同 @exception
{@value} 显示常量的值,该常量必须是static {@value #a} 是一个常量
@version 指定类的版本 @version 0.0.1

正则表达式匹配词

字符 描述
\ 将下一个字符标记为一个特殊字符、或一个原义字符、或一个向后引用、或一个八进制转义符。例如,“n”匹配字符“n”。“\n”匹配一个换行符。串行“\\”匹配“\”而“\(”则匹配“(”。
^ 匹配输入字符串的开始位置。如果设置了RegExp对象的Multiline属性,^也匹配“\n”或“\r”之后的位置。
$ 匹配输入字符串的结束位置。如果设置了RegExp对象的Multiline属性,$也匹配“\n”或“\r”之前的位置。
* 匹配前面的子表达式零次或多次。例如,zo能匹配“z”以及“zoo”。等价于{0,}。
+ 匹配前面的子表达式一次或多次。例如,“zo+”能匹配“zo”以及“zoo”,但不能匹配“z”。+等价于{1,}。
? 匹配前面的子表达式零次或一次。例如,“do(es)?”可以匹配“does”或“does”中的“do”。?等价于{0,1}。
{n} n是一个非负整数。匹配确定的n次。例如,“o{2}”不能匹配“Bob”中的“o”,但是能匹配“food”中的两个o。
{n,} n是一个非负整数。至少匹配n次。例如,“o{2,}”不能匹配“Bob”中的“o”,但能匹配“foooood”中的所有o。“o{1,}”等价于“o+”。“o{0,}”则等价于“o*”。
{n,m} m和n均为非负整数,其中n<=m。最少匹配n次且最多匹配m次。例如,“o{1,3}”将匹配“fooooood”中的前三个o。“o{0,1}”等价于“o?”。请注意在逗号和两个数之间不能有空格。
? 当该字符紧跟在任何一个其他限制符(*,+,?,{n},{n,},{n,m})后面时,匹配模式是非贪婪的。非贪婪模式尽可能少的匹配所搜索的字符串,而默认的贪婪模式则尽可能多的匹配所搜索的字符串。例如,对于字符串“oooo”,“o+?”将匹配单个“o”,而“o+”将匹配所有“o”。
. 匹配除“``n”之外的任何单个字符。要匹配包括“``n”在内的任何字符,请使用像“(.|\n)”的模式。
(pattern) 匹配pattern并获取这一匹配。所获取的匹配可以从产生的Matches集合得到,在VBScript中使用SubMatches集合,在JScript中则使用$0…$9属性。要匹配圆括号字符,请使用“(”或“)”。
(?:pattern) 匹配pattern但不获取匹配结果,也就是说这是一个非获取匹配,不进行存储供以后使用。这在使用或字符“(|)”来组合一个模式的各个部分是很有用。例如“industr(?:y|ies)”就是一个比“industry|industries”更简略的表达式。
(?=pattern) 正向肯定预查,在任何匹配pattern的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。例如,“Windows(?=95|98|NT|2000)”能匹配“Windows2000”中的“Windows”,但不能匹配“Windows3.1”中的“Windows”。预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始。
(?!pattern) 正向否定预查,在任何不匹配pattern的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。例如“Windows(?!95|98|NT|2000)”能匹配“Windows3.1”中的“Windows”,但不能匹配“Windows2000”中的“Windows”。预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始
(?<=pattern) 反向肯定预查,与正向肯定预查类拟,只是方向相反。例如,“(?<=95|98|NT|2000)Windows”能匹配“2000Windows”中的“Windows”,但不能匹配“3.1Windows”中的“Windows”。
(?<!pattern) 反向否定预查,与正向否定预查类拟,只是方向相反。例如“(?<!95|98|NT|2000)Windows”能匹配“3.1Windows”中的“Windows”,但不能匹配“2000Windows”中的“Windows”。
x|y 匹配x或y。例如,“z|food”能匹配“z”或“food”。“(z|f)ood”则匹配“zood”或“food”。
[xyz] 字符集合。匹配所包含的任意一个字符。例如,“[abc]”可以匹配“plain”中的“a”。
[^xyz] 负值字符集合。匹配未包含的任意字符。例如,“[^abc]”可以匹配“plain”中的“p”。
[a-z] 字符范围。匹配指定范围内的任意字符。例如,“[a-z]”可以匹配“a”到“z”范围内的任意小写字母字符。
[^a-z] 负值字符范围。匹配任何不在指定范围内的任意字符。例如,“[^a-z]”可以匹配任何不在“a”到“z”范围内的任意字符。
\b 匹配一个单词边界,也就是指单词和空格间的位置。例如,“er\b”可以匹配“never”中的“er”,但不能匹配“verb”中的“er”。
\B 匹配非单词边界。“er\B”能匹配“verb”中的“er”,但不能匹配“never”中的“er”。
\cx 匹配由x指明的控制字符。例如,\cM匹配一个Control-M或回车符。x的值必须为A-Z或a-z之一。否则,将c视为一个原义的“c”字符。
\d 匹配一个数字字符。等价于[0-9]。
\D 匹配一个非数字字符。等价于[^0-9]。
\f 匹配一个换页符。等价于\x0c和\cL。
\n 匹配一个换行符。等价于\x0a和\cJ。
\r 匹配一个回车符。等价于\x0d和\cM。
\s 匹配任何空白字符,包括空格、制表符、换页符等等。等价于[ \f\n\r\t\v]。
\S 匹配任何非空白字符。等价于[^ \f\n\r\t\v]。
\t 匹配一个制表符。等价于\x09和\cI。
\v 匹配一个垂直制表符。等价于\x0b和\cK。
\w 匹配包括下划线的任何单词字符。等价于“[A-Za-z0-9_]”。
\W 匹配任何非单词字符。等价于“[^A-Za-z0-9_]”。
\xn 匹配n,其中n为十六进制转义值。十六进制转义值必须为确定的两个数字长。例如,“\x41”匹配“A”。“\x041”则等价于“\x04&1”。正则表达式中可以使用ASCII编码。.
\num 匹配num,其中num是一个正整数。对所获取的匹配的引用。例如,“(.)\1”匹配两个连续的相同字符。
\n 标识一个八进制转义值或一个向后引用。如果\n之前至少n个获取的子表达式,则n为向后引用。否则,如果n为八进制数字(0-7),则n为一个八进制转义值。
\nm 标识一个八进制转义值或一个向后引用。如果\nm之前至少有nm个获得子表达式,则nm为向后引用。如果\nm之前至少有n个获取,则n为一个后跟文字m的向后引用。如果前面的条件都不满足,若n和m均为八进制数字(0-7),则\nm将匹配八进制转义值nm。
\nml 如果n为八进制数字(0-3),且m和l均为八进制数字(0-7),则匹配八进制转义值nml。
\un 匹配n,其中n是一个用四个十六进制数字表示的Unicode字符。例如,\u00A9匹配版权符号(©)。