📚博客主页:爱敲代码的小杨.
✨专栏:《Java SE语法》 | 《数据结构与算法》 | 《C生万物》
❤️感谢大家点赞👍🏻收藏⭐评论✍🏻,您的三连就是我持续更新的动力❤️
🙏小杨水平有限,欢迎各位大佬指点,相互学习进步!
1. 初始 java
1.1 Java 概述
1.1.1什么是 Java
Java是一种优秀的程序设计语言,它具有令人赏心悦目的语法和易于理解的语义。
不仅如此,Java还是一个有一系列计算机软件和规范形成的技术体系,这个技术体系提供了完整的用于软件开发和跨平台部署的支持环境,并广泛应用于嵌入式系统、移动终端、企业服务器、大型机等各种场合。
1.1.2 Java 背景
- Java 是美国sun公司在1995年推出的一门计算机高级编程语言。
- Java 早期称为Oak(橡树),后期改名Java。
- Java 之父:詹姆斯·高斯林(祖师爷)。
- 2009年 sun公司被 Oracle(甲骨文)公司收购。
1.1.3 为什么使用 Java
- 世界上最流行的编程语言之一,在国内使用最为广泛的编程语言
- 可移植性、安全可靠、性能较好
- 开发社区最完善、功能最丰富
1.1.4 Java能做什么
- 桌面应用开发
- 企业级应用开发
- 移动应用开发
- 服务器系统
- 大数据开发
- 游戏开发
1.1.5 Java 技术体系
技术体系 | 说明 |
---|---|
Java SE:标准版 | Java技术的核心和基础 |
Java EE:企业版 | 企业级应用开发的一套解决方案 |
Java ME:小型版 | 针对移动设备应用的解决方案 |
1.1.6 Java 发展史
1.1.7 Java的主要特征
以下 Java 特性来自 Java 白皮书
- 简单性:
Java 语言的语法与C语言和C++语言很接近,使得大多数程序员很容易学习和使用,另一方面,Java 丢弃了C++中很少使用的、很难理解的、令人迷惑的那些特性,如操作符重载、多继承、自动的强制类型转换。特别地,Java语言不使用指针,而是引用。并提供了自动的废料收集,使得程序员不必为内存管理而担忧。
- 面向对象:
Java语言提供类、接口和继承等面向对象的特性,为了简单起见,只支持类之间的单继承,但支持接口之间的实现机制(关键字为implements)。Java语言全面支持动态绑定,而C++语言只对虚函数使用动态绑定。总之,Java语言是一个纯的面向对象程序设计语言
- 分布式:
Java有一个丰富的例程库,用于处理像 HTTP 和FTP之类的TCP/IP协议。
Java应用程序能够通过URL打开和访问网络上的对象,其便捷程序就好像访问本地文件一样。
- 健壮性:
Java 的设计目标之一在于使得 Java 编写的程序具有多方面的可靠性。Java 非常强调进行早期的问题检测、后期动态的(运行时)检测,以及消除容易出错的情况… Java 与C/C++ 最大的不同在于Java 采用的指针模型可以消除重写内存和损坏数据的可能性。
- 安全性:
Java 要适用于网络/分布式环境。为了实现这个目标,安全性颇受重视。使用Java 可以构建放病毒、防篡改的系统。
Java 设计能够防范各种攻击,其中包括:
- 运行时堆栈溢出,这是蠕虫和病毒常用的攻击手段。
- 破坏自己的进程空间之外的内存,
- 未经授权读写文件
- 体系结构中立:
Java程序(后缀为java的文件)在Java平台上被编译为体系结构中立的字节码格式(后缀为class的文件),然后可以在实现这个Java平台的任何系统中运行。这种途径适合于异构的网络环境和软件的分发。
- 可移植性:
Java程序(后缀为java的文件)在Java平台上被编译为体系结构中立的字节码格式(后缀为class的文件),然后可以在实现这个Java平台的任何系统中运行。这种途径适合于异构的网络环境和软件的分发
- 解释型:
如前所述,Java程序在Java平台上被编译为字节码格式,然后可以在实现这个Java平台的任何系统中运行。在运行时,Java平台中的Java解释器对这些字节码进行解释执行,执行过程中需要的类在联接阶段被载入到运行环境中。
- 高性能:
与那些解释型的高级脚本语言相比,Java的确是高性能的。事实上,Java的运行速度随着JIT(Just-In-Time)编译器技术的发展越来越接近于C++
- 多线程:
在Java语言中,线程是一种特殊的对象,它必须由Thread类或其子(孙)类来创建。通常有两种方法来创建线程:其一,使用型构为Thread(Runnable)的构造子类将一个实现了Runnable接口的对象包装成一个线程,其二,从Thread类派生出子类并重写run方法,使用该子类创建的对象即为线程。值得注意的是Thread类已经实现了Runnable接口,因此,任何一个线程均有它的run方法,而run方法中包含了线程所要运行的代码。线程的活动由一组方法来控制。Java语言支持多个线程的同时执行,并提供多线程之间的同步机制(关键字为synchronized)
- 动态性:
Java语言的设计目标之一是适应于动态变化的环境。Java程序需要的类能够动态地被载入到运行环境,也可以通过网络来载入所需要的类。这也有利于软件的升级。另外,Java中的类有一个运行时刻的表示,能进行运行时刻的类型检查
因此:Java不仅仅是一门编程语言,也是一个由一些列计算机软件和规范组成的技术体系。
1.2 JDK
sum 公司提供了一套 Java 开发环境,简称 JDK(Java Java Development Kit)。JDK 包括 Java编译器、Java运行工具、Java 文档生成工具、Java 打包工具等。
sum 公司除了提供 JDK外,还提供了一种 JRE(Java Runtime Environment)工具,它是提供给普通用户使用的 Java 运行环境。与 JDK 相比,JRE 工具只包含 Java运行工具,不包含 Jav 编译工具。需要说明的是,为了方便使用,sum 公司在 JDK 工具总封装了一个 JRE 工具,即开发环境中包含了运行环境。
1.2.1 安装 JDK和配置环境变量
JDK安装教程
IntelliJ IDEA安装教程
1.2.2 JDK 目录介绍
JDK 安装完毕后,会在磁盘上生成一个目录,该目录被称为 JDK 目录。
PS:可能有些小伙伴会疑问自己为什么没有src这个文件夹,这里是因为博主解压了src这个压缩包。
-
bin
目录:该目录用于存放一些可执行程序,如:javac.exe
(Java 编译器)、java.exe
(Java 运行工具)、jar
(打包工具)和javadoc.exe
(文档生成工具)等。其中,最重要就是javac.exe
和java.exe
,下面我们对这两个程序进行讲解:-
javac.exe
是 Java 编译器,它可以将编写的 Java 文件编译成 Java 字节码文件(可执行的 Java 程序)。 -
java.exe
是 Java 运行工具,它会启动一个 Java 虚拟机(JVM)进程,Java 虚拟机相当于一个虚拟的系统,专门运行由 Java 编译器生成的字节码文件(.class
文件)
-
-
include
目录:由于 JDK 是使用 C/C++ 开发的,因此在启动时需要引入一些C语言的头文件,该目录就是用于存放这些头文件的。 -
jre
目录:jre 是 Java Runtime Environment 的缩写,意味 Java 程序运行时的环境。该目录是 Java 运行时环境的根目录,它包含 Java 虚拟机、运行时的类包、Java 应用启动器和一个bin 目录,但不包含开发环境中的开发工具。 -
lib
目录:lib 是 library 的缩写,意为 Java 类库或库文件,是开发工具使用的归档包文件。 -
src.zip
和javafx-src.zip
文件:这两个文件中放置的是 JDK 核心类的源代码和 JavaFX 源代码,通过这两个文件可以查看 Java 基础类的源代码。
1.2.3 【面试题】:JDK、JRE、JVM之间的关系?*
- JDK(Java Development Kit):Java开发工具包,提供给Java程序员使用,包含了JRE,同时还包含了编译
器javac与自带的调试工具Jconsole、jstack等。 - JRE(Java Runtime Environment):Java运行时环境,包含了JVM,Java基础类库。是使用Java语言编写程
序运行的所需环境。 - JVM:Java虚拟机,运行Java代码
1.2.4 Java 跨平台、工作原理
- 一次编译,处处可用
- Java文件编译生成跟平台无关的字节码文件(class文件)
- JVM:Java虚拟机
- 由对应平台的JVM解析字节码为机器指令。
1.3 Java 程序开发
Java 程序,需要三个步骤:编写程序,编译程序,运行程序
1. 编写 Java 源文件
public class Main {
public static void main(String[] args) {
System.out.println("Hello world!");
}
}
通过上述代码,我们可以看到一个完整的Java程序的结构,Java程序的结构由如下三个部分组成:
-
源文件(扩展名为*.java):源文件带有类的定义。类用来表示程序的一个组件,小程序或许只会有一个
类。类的内容必须包含在花括号里面。 - 类:类中带有一个或多个方法。方法必须在类的内部声明。
- 方法:在方法的花括号中编写方法应该执行的语句。
总结:类存在于源文件里面;方法存在于类中;语句存在于方法中。
注意:在一个源文件中只能有一个public修饰的类,而且源文件名字必须与public修饰的类名字相同。
2. 运行 Java 程序
Java是一门半编译型、半解释型语言。先通过javac
编译程序把源文件进行编译,编译后生成的.class文件是由字节码组成的平台无关、面向JVM的文件。最后启动java
虚拟机来运行.class文件,此时JVM会将字节码转换成平台能够理解的形式来运行。
-
编译程序:
javac 文件名.java
javac HelloWorld.java
-
运行程序:
java 文件名
java HelloWorld
注意事项:
- 第一个Java程序尽量用记事本写
- 建议代码文件名全英文,首字母大写,满足驼峰模式,源代码文件后缀为.java
1.4 注释
1.4.1 什么注释
注释是写程序中对代码进行解释说明的文字,方便自己和其他人查看,以便理解程序的。
1.4.2 注释有哪些
- 单行注释://
- 多行注释:/* */
- 文档注释:/** */
1.4.3 注释的特点
注释不影响程序的执行,编译后的class文件里面没有注释
1.5 标识符
标识符:在程序中由用户给类名、方法名或者变量所取的名字。
【硬性规则】
标识符中可以包含:字母、数字以及下划线和 $ 符号等等
注意:标识符不能以数字开头,不能是关键字,且严格区分大小写。
【软性建议】
- 类名:每个单词的首字母大写(大驼峰)
- 方法名:首字母小写,后面每个单词的首字母大写(小驼峰)
- 变量名:与方法名规则相同。
1.6 关键字
关键字是由Java语言提前定义好的,有特殊含义的标识符,或者保留字
注意:用户不能使用关键字定义标识符
2. 数据类型和变量
2.1 字面常量
常量即程序运行时期,固定不变的量称为常量。
public class Test {
public static void main(String[] args) {
System.out.println("Hello world!");
System.out.println(100);
System.out.println(3.14);
System.out.println('A');
System.out.println(true);
System.out.println(false);
}
}
其中,“Hello world!”,100,3.14,‘A’,true/false都是常量,将其称为字面常量。
字面量的分类:
- 字符串常量:由"“括起来的,比如"12345”、“hello”、“你好”。
- 整型常量:程序中直接写的数字(注意没有小数点),比如:100、1000
- 浮点型常量:程序中直接写的小数,比如:3.14、0.49
- 字符常量:由’'括起来的单个字符,如:‘A’,‘1’
- 布尔常量:只有两种
true
和false
- 空常量:
null
注意:字符串、整型、浮点型、字符型以及不而行,在Java中都称为数据类型。
2.2 数据类型
Java 是一种强类型语言。这意味着必须每一个变量声明一个类型。
Java 中数据类型主要分为两类:基本数据类型和引用数据类型。
数据类型 | 关键字 | 内存占用 | 范围 |
---|---|---|---|
字节型 | byte | 1字节 | -128~127 |
短整型 | short | 2字节 | -32768~32767 |
整型 | int | 4字节 | -231~231-1 |
长整型 | long | 8字节 | -263~263-1 |
单精度浮点数 | float | 4字节 | 有范围,一般不关注 |
双精度浮点数 | double | 8字节 | 有范围,一般不关注 |
字符型 | char | 2字节 | 0~65535 |
布尔型 | boolean | 没有明确规定 | true和false |
注意:
- 无论是在32为系统还是64为系统,int都占用4个字节,long都占8个字节
- 整型和浮点型都是带有符号的
- 整型默认是int型,浮点型默认是double
- 字符串属于引用类型,后序介绍。
什么是字节?
字节是计算机中表示空间大小的基本单位.
计算机使用二进制表示数据. 我们认为 8 个二进制位(bit) 为一个字节(Byte).
我们平时的计算机为16GB 内存, 意思是16G 个字节.
其中 1KB = 1024 Byte, 1MB = 1024 KB, 1GB = 1024 MB.
所以 16GB 相当于 160 多亿个字节.
2.3 变量
2.3.1 变量的概念
在程序中,除了有始终不变的常量外,有些内容可以回经常改变,比如:人的年龄、身高、成绩分数、数学函数的计算结果等,对于这些经常改变的内容,在 Java 中称为变量。而数据类型就是用来定义不同种类变量
2.3.2 语法格式
定义变量的语法格式为:
数据类型 变量名 = 初始值;
比如:
public class Test {
public static void main(String[] args) {
int a = 10; // 定义整形变量a,a是变量名也称为标识符,该变量中放置的值为10
double d = 3.14;
char c = 'A';
Boolean b = true;
System.out.println(a);
System.out.println(d);
System.out.println(c);
System.out.println(b);
a = 100;
// a是变量,a中的值是可以修改的,
// 注意:= 在java中表示赋值,即将100交给a,a中保存的值就是100
System.out.println(a);
// 注意:在一行可以定义多个相同类型的变量
int a1 = 10,a2 = 20, a3 = 30;
System.out.println(a1);
System.out.println(a2);
System.out.println(a3);
}
}
2.3.3 整型变量
2.3.3.1 整型变量
-
方法一:定义时给出初始值
// 方法一:定义时给出初始值 int a = 10; System.out.println(a);
-
方法二:定义时没有给初始值,但使用前必须设置初值
// 方法二:定义时没有给初始值,但使用前必须设置初值 int b; b = 20; System.out.println(b); // 使用方式二定义后,在使用前如果没有赋值,则编译期间会报错 int c; System.out.println(c);
-
int型变量所能表示的范围:
// int型变量所能表示的范围: System.out.println(Integer.MIN_VALUE);// -2147483648 System.out.println(Integer.MAX_VALUE);// 2147483647 // 注意:在定义int性变量时,所赋值不能超过int的范围 int d = 12345678901234; // 编译时报错,初值超过了int的范围 System.out.println(d);
注意事项:
- int不论在何种系统下都是4个字节
- 推荐使用方式一定义,如果没有合适的初始值,可以设置为0
- 在给变量设置初始值时,值不能超过int的表示范围,否则会导致溢出
- 变量在使用之前必须要赋初值,否则编译报错
- int的包装类型为 Integer
2.3.3.2 长整型变量
int a = 10;
long b = 10; // long定义的长整型变量
long c = 10L; // 为了区分int和long类型,一般建议:long类型变量的初始值之后加L或者l
long d = 10l; // 一般更加以加大写L,因为小写l与1不好区分
// long型变量所能表示的范围:这个数据范围远超过 int 的表示范围. 足够绝大部分的工程场景使用.
System.out.println(Long.MIN_VALUE);// -9223372036854775808
System.out.println(Long.MAX_VALUE);// 9223372036854775807
注意事项:
- 长整型变量的初始值后加L或者l,推荐加L
- 长整型不论在那个系统下都占8个字节
- 长整型的表示范围为:-263~263-1
- long的包装类型为Long
2.3.3.3 短整型变量
short a = 10;
System.out.println(a);
// short型变量所能表示的范围:
System.out.println(Short.MIN_VALUE);// -32768
System.out.println(Short.MAX_VALUE);// 32767
注意事项:
- short在任何系统下都占2个字节
- short的表示范围为:-32768 ~ 32767
- 使用时注意不要超过范围(一般使用比较少)
- short的包装类型为Short
2.3.3.4 字节型变量
byte b = 10;
System.out.println(b);
// byte型变量所能表示的范围:
System.out.println(Byte.MIN_VALUE);// -128
System.out.println(Byte.MAX_VALUE);// 127
注意事项:
- byte在任何系统下都占1个字节
- byte的范围是:-128 ~ 127 3. 字节的包装类型为Byte
2.3.4 浮点型变量
2.3.4.1 双精度浮点型
double d = 3.14;
System.out.println(d);
代码一:
int a = 1;
int b = 2;
System.out.println(a / b); // 0
// 为什么不输出0.5?
在 Java 中,int
除以int
的值仍然是int
(会直接舍弃小数部分)。如果想要得到0.5,需要使用double
类型计算。
double a = 1.0;
double b = 2.0;
System.out.println(a / b);// 0.5
代码二:
double num = 1.1;
System.out.println(num * num); // 输出1.21?
// 1.2100000000000002
注意事项:
- double在任何系统下都占8个字节
- 浮点数与整数在内存中的存储方式不同,不能单纯使用的形式来计算
- double的包装类型为Double
- double 类型的内存布局遵守 IEEE 754 标准(和C语言一样), 尝试使用有限的内存空间表示可能无限的小数, 势 必会存在一定的精度误差,因此浮点数是个近似值,并不是精确值
2.3.4.2 单精度浮点型
loat num = 1.0f; // 写作 1.0F 也可以
System.out.println(num);
注意事项:
- float 类型在 Java 中占四个字节, 同样遵守 IEEE 754 标准.
- 由于表示的数据精度范围较小, 一般在工程上用到浮点数 都优先考虑 double, 不太推荐使用 float.
- float的包装类型为Float。
2.3.5 字符型变量
char c1 = 'A'; // 大写字母
char c2 = '1'; // 数字字符
System.out.println(c1);
System.out.println(c2);
// 注意:java中的字符可以存放整形
char c3 = '帅';
System.out.println(c3);
注意事项:
-
Java 中使用 单引号 + 单个字母 的形式表示字符字面值.
-
计算机中的字符本质上是一个整数. 在 C 语言中使用 ASCII 表示字符, 而 Java 中使用 Unicode 表示字符. 因此 一个字符占用两个字节, 表示的字符种类更多, 包括中文.
char c3 = '帅'; System.out.println(c3);
-
char的包装类型为Characte
2.3.6 布尔类型变量
布尔类型常用来表示真假,在现实生活中也是经常出现的,比如:听说xxx同学买彩票中了一个亿…,听到后估计 大部分人第一反应就是:我x,真的假的?
boolean b = true;
System.out.println(b);
b = false;
System.out.println(b);
注意事项:
-
boolean 类型的变量只有两种取值, true 表示真, false 表示假.
-
Java 的 boolean 类型和 int 不能相互转换, 不存在
1
表示true
,0
表示false
这样的用法.
-
Java虚拟机规范中,并没有明确规定boolean占几个字节,也没有专门用来处理boolean的字节码指令,在 Oracle公司的虚拟机实现中,boolean占1个字节.
-
boolean的包装类型为Boolean。
2.3.7 类型转换
我们经常需要将一种数据类型转换为另一种数据类型。
图中6个实线箭头,表示无信息丢失的转换;另外有3个虚线剪头,表示可能有精度损失的转换。
2.3.7.1 自动类型转换(隐式)
自动类型转换即:代码不需要经过任何处理,在代码编译时,编译器会自动进行处理。特点:数据范围小的转为数据范围大的时会自动进行。
System.out.println(1024); // 整型默认情况下是int
System.out.println(3.14); // 浮点型默认情况下是double
int a = 100;
long b = 10L;
b = a;
// a和b都是整形,a的范围小,b的范围大,当将a赋值给b时,编译器会自动将a提升为long类型,然后赋值
a = b;
// 编译报错,long的范围比int范围大,会有数据丢失,不安全
float f = 3.14F;
double d = 5.12;
d = f; // 编译器会将f转换为double,然后进行赋值
f = d; // double表示数据范围大,直接将float交给double会有数据丢失,不安全
byte b1 = 100; // 编译通过,100没有超过byte的范围,编译器隐式将100转换为byte
byte b2 = 257; // 编译失败,257超过了byte的数据范围,有数据丢失
2.3.7.2 强制类型转换(显式)
强制类型转换:当进行操作时,代码需要经过一定的格式处理,不能自动完成。特点:数据范围大的到数据范围小的。
int a = 10;
long b = 100L;
b = a; // int-->long,数据范围由小到大,隐式转换
a = (int)b; // long-->int, 数据范围由大到小,需要强转,否则编译失败
float f = 3.14F;
double d = 5.12;
d = f; // float-->double,数据范围由小到大,隐式转换
f = (float)d; // double-->float, 数据范围由大到小,需要强转,否则编译失败
a = d; // 报错,类型不兼容
a = (int)d; // int没有double表示的数据范围大,需要强转,小数点之后全部丢弃
byte b1 = 100; // 100默认为int,没有超过byte范围,隐式转换
byte b2 = (byte)257; // 257默认为int,超过byte范围,需要显示转换,否则报错
boolean flag = true;
a = flag; // 编译失败:类型不兼容
flag = a; // 编译失败:类型不兼容
注意事项:
- 不同数字类型的变量之间赋值, 表示范围更小的类型能隐式转换成范围较大的类型
- 如果需要把范围大的类型赋值给范围小的, 需要强制类型转换, 但是可能精度丢失
- 将一个字面值常量进行赋值的时候, Java 会自动针对数字范围进行检查
- 强制类型转换不一定能成功,不相干的类型不能互相转换
2.3.8 类型提升
不同类型的数据之间相互运算时,数据类型小的会被提升到数据类型大的。
-
int与long之间:int会被提升为long
int a = 10; long b = 20; int c = a + b; // 编译出错: a + b==》int + long--> long + long 赋值给int时会丢失数据 long d = a + b; // 编译成功:a + b==>int + long--->long + long 赋值给long
-
byte与byte的运算
byte a = 10; byte b = 20; byte c = a + b; System.out.println(c);
结论: byte
和 byte
都是相同类型, 但是出现编译报错. 原因是, 虽然 a 和 b 都是 byte
, 但是计算 a + b
会先将 a 和 b 都提升成 int
, 再进行计算, 得到的结果也是 int
, 这是赋给 c, 就会出现上述错误.
由于计算机的 CPU 通常是按照 4 个字节为单位从内存中读写数据. 为了硬件上实现方便, 诸如 byte 和 short 这种低于 4 个字节的类型, 会先提升成 int, 再参与计算
正确的写法:
byte a = 10;
byte b = 20;
byte c = (byte)(a + b);
System.out.println(c);
【类型提升小结:】
- 不同类型的数据混合运算, 范围小的会提升成范围大的.
- 对于 short, byte 这种比 4 个字节小的类型, 会先提升成 4 个字节的 int , 再运算.
2.4 字符串类型
在Java中使用String类定义字符串类型,比如:
String s1 = "hello";
String s2 = " world";
System.out.println(s1);
System.out.println(s2);
System.out.println(s1+s2); // s1+s2表示:将s1和s2进行拼接
在有些情况下,需要将字符串和整形数字之间进行转换:
-
int
转成String
int num = 10; // 方法1 String str1 = num + ""; // 方法2 String str2 = String.valueOf(num);
-
String
转成int
String str = "100"; int num = Integer.parseInt(str);
本节对只是对字符串进行简单的介绍,大家能够正常使用即可,后序会详细给大家介绍。
3. 运算符
3.1 什么是运算符
计算机的最基本的用途之一就是执行数学运算,比如:
int a = 10;
int b = 20;
a + b;
a < b;
上述 +
和 <
等就是运算符,即:对操作数进行操作时的符号,不同运算符操作的含义不同。
作为一门计算机语言,Java也提供了一套丰富的运算符来操纵变量。Java中运算符可分为以下:算术运算符(+ - * /)、关系运算符(< > ==)、逻辑运算符、位运算符、移位运算符以及条件运算符等
3.2 算术运算符
-
基本四则运算:加减乘除求模(
+
,-
,*
,/
,%
)int a = 20; int b = 10; System.out.println(a + b); // 30 System.out.println(a - b); // 10 System.out.println(a * b); // 200 System.out.println(a / b); // 2 System.out.println(a % b); // 0 --->模运算相当于数学中除法的余数
【注意】:
-
都是二元运算符,使用时必须要有左右两个操作数
-
int
/int
结果还是int类型,而且会向下取整int a = 3; int b = 2; // 在数学中应该是1.5 但是在Java中输出结果为1 会向下取整,即小数点之后全部舍弃掉了 System.out.println(a / b); // 1 // 如果要得到数学中的结果,可以使用如下方式 double d = a * 1.0 / b; System.out.println(d);// 1.5
-
做除法和取模时,右操作数不能为0
-
% 不仅可以对整型取模,也可以对double类型取模,但是没有意义,一般都是对整型取模的
System.out.println(11.5 % 2.0);// 1.5
-
两侧操作数类型不一致时,向类型大的提升
System.out.println(1 + 0.2); // +的左侧是int,右侧是double,在加之前int被提升为double // 1.2
-
-
结合赋值和运算符(
+=
,-=
,*=
,/=
,%=
)该种类型运算符操作完成后,会将操纵的结果赋值给左操作数.
int a = 1; a += 2; // 相当于 a = a + 2 System.out.println(a); // 输出3 a -= 1;// 相当于 a = a - 1 System.out.println(a); // 输出2 a *= 3;// 相当于 a = a * 3 System.out.println(a); // 输出6 a /= 3;// 相当于 a = a / 3 System.out.println(a); // 输出2 a %= 3; // 相当于 a = a % 2 System.out.println(a); // 输出2
【注意】:只有变量才能使用该运算符,常量不能使用。
-
自增/自减运算符(
++
,--
)++是一种自增的操作符,又分为前置++和后置++,–是一种自增的操作符,又分为前置–-和后置–-。
- 前置
++
public class Test1 { public static void main(String[] args) { int a = 10; int b = ++a;// ++的操作数是a,是放在a的前面的,就是前置++ System.out.printf("a = %d , b = %d",a , b); // 运行结果:a = 11 , b = 11 } }
计算口诀:先 +1,后使用
a原来是10,先 +1,后a变成了11,再使用赋值给b,b得到的也是11,所以计算后,a和b都是11,等价于这样的代码:public class Test1 { public static void main(String[] args) { int a = 10; a += 1; int b = a; System.out.printf("a = %d , b = %d",a , b); // 运行结果:a = 11 , b = 10 } }
- 后置
++
public class Test1 { public static void main(String[] args) { int a = 10; int b = a++;// ++的操作数是a,是放在a的后面的,就是后置++ System.out.printf("a = %d , b = %d",a , b); // 运行结果:a = 11 , b = 10 } }
计算口诀:先使用,后 +1
a原来是10,先使用,把a赋值给b,b变成了10,后a+1变成了10,所以计算后,a=11,b=10,等价于这样的代码:public class Test1 { public static void main(String[] args) { int a = 10; int b = a; a += 1; System.out.printf("a = %d , b = %d",a , b); // 运行结果:a = 11 , b = 10 } }
- 前置
--
和前置++同理,只是换成了-1
计算口诀:先 -1,后使用public class Test1 { public static void main(String[] args) { int a = 10; int b = --a;// --的操作数是a,是放在a的前面的,就是前置-- System.out.printf("a = %d , b = %d",a , b); // 运行结果:a = 9 , b = 9 } }
- 后置
--
和后置++同理,只是换成了-1
计算口诀:先使用,后-1public class Test1 { public static void main(String[] args) { int a = 10; int b = a--;// --的操作数是a,是放在a的后面的,就是后置-- System.out.printf("a = %d , b = %d",a , b); // 运行结果:a = 9 , b = 10 } }
【注意】:
- 如果混合使用,【前置++】先+1,然后使用变量+1之后的值,【后置++】先使用变量原来的值,表达式 结束时给变量+1
- 只有变量才能使用自增/自减运算符,常量不能使用,因为常量不允许被修改
- 前置
3.3 关系运算符
关系远算符有6个:(==
,!=
,>
,<
,>=
,<=
),其结果是true
和flase
int a = 10;
int b = 20;
// 注意:在Java中 = 表示赋值,要与数学中的含义区分
//在Java中 == 表示相等
System.out.println(a == b); // false
System.out.println(a != b); // true
System.out.println(a < b); // true
System.out.println(a > b); // false
System.out.println(a <= b); // true
System.out.println(a >= b); // false
【注意】:当需要多次判断时,不能连着写,比如:3 < a < 5,Java程序与数学中是有区别的
3.4 逻辑运算符(重点)
逻辑远算符主要由3个:(&&
,||
,!
),运算结果都是 boolean
类型。
-
逻辑与 &&
语法规则:
表达式1
&&表达式2
,左右表达式必须是boolean
类型的结果。两个表达式都为真,结果才是真,只要有一个是假,结果就是假。
表达式1 表达式2 结果 真 真 真 真 假 假 假 真 假 假 假 假 int a = 1; int b = 2; System.out.println(a == 1 && b == 2); // 左为真 且 右为真 则结果为真 System.out.println(a == 1 && b > 100); // 左为真 但 右为假 则结果为假 System.out.println(a > 100 && b == 2); // 左为假 但 右为真 则结果为假 System.out.println(a > 100 && b > 100); // 左为假 且 右为假 则结果为假
-
逻辑或 ||
语法规则:
表达式1
||表达式2
,左右表达式必须是boolean
类型的结果。两个表达式都为假,结果才是假,只要由一个是真,结果就是真。
表达式1 表达式2 结果 真 真 真 真 假 真 假 真 真 假 假 假 int a = 1; int b = 2; System.out.println(a == 1 || b == 2); // 左为真 且 右为真 则结果为真 System.out.println(a == 1 || b > 100); // 左为真 但 右为假 则结果也为真 System.out.println(a > 100 || b == 2); // 左为假 但 右为真 则结果也为真 System.out.println(a > 100 || b > 100); // 左为假 且 右为假 则结果为假
-
逻辑非 !
语法规则:! 表达式
真变假,假变真。
表达式 结果 真 假 假 真 int a = 1; System.out.println(!(a == 1)); // a == 1 为true,取个非就是false System.out.println(!(a != 1)); // a != 1 为false,取个非就是true
-
短路求值
&& 和 || 遵守短路求值的规则.
System.out.println(10 > 20 && 10 / 0 == 0); // 打印 false System.out.println(10 < 20 || 10 / 0 == 0); // 打印 true
我们都知道, 计算
10 / 0
会导致程序抛出异常. 但是上面的代码却能正常运行, 说明10 / 0
并没有真正被求值.【注意】:
-
对于 && , 如果左侧表达式值为 false, 则表达式结果一定是 false, 无需计算右侧表达式
-
对于 ||, 如果左侧表达式值为 true, 则表达式结果一定是 true, 无需计算右侧表达式.
-
& 和 | 如果表达式结果为 boolean 时, 也表示逻辑运算. 但与 && || 相比, 它们不支持短路求值.
记忆口诀:
&&:全真为真,有假必假,遇假则停
||:全假为假,有真必真,遇真则停
!:真变假,假变真
-
3.5 位运算符
Java 中数据存储的最小单位是字节,而数据操作的最小单位是比特位. 字节是最小的存储单位,每个字节是由8个二进制比特位组成的,多个字节组合在一起可以表示各种不同的数据。
位运算符主要有四个:&
,|
, ~
, ^
,除~
是一元运算符外,其余都是二元运算符。
位操作表示 按二进制位运算. 计算机中都是使用二进制来表示数据的(01构成的序列), 按位运算就是在按照二进制位 的每一位依次进行计算。
-
按位与 &: 如果两个二进制位都是 1, 则结果为 1, 否则结果为 0
int a = 10; int b = 20; System.out.println(a & b); // 0000 1010 10的二进制 // 0001 0100 20的二进制 // 0000 0000
-
按位或 |: 如果两个二进制位都是 0, 则结果为 0, 否则结果为 1
int a = 10; int b = 20; System.out.println(a | b); // 0000 1010 // 0001 0100 // 0001 1110
-
.按位取反 ~: 如果该位为 0 则转为 1, 如果该位为 1 则转为 0
int a = 0xf; System.out.printf("%x\n", ~a);
【注意】:
- 0x 前缀的数字为 十六进制 数字. 十六进制可以看成是二进制的简化表示方式. 一个十六进制数字对应 4 个二进制位.
- 0xf 表示 10 进制的 15, 也就是二进制的 1111
- printf 能够格式化输出内容, %x 表示按照十六进制输出.
- \n 表示换行符
-
按位异或 ^: 如果两个数字的二进制位相同, 则结果为 0, 相异则结果为 1
int a = 0x1; int b = 0x2; System.out.printf("%x\n", a ^ b);
【注意】:如果两个数相同,则异或的结果为0
3.6 移位运算符(了解)
移位运算符有三个:<<
,>>
,>>>
,都是二元运算符,且都是按照二进制比特位来运算的。
-
左移 <<: 最左侧位不要了, 最右侧补 0
int a = 0x10; System.out.printf("%x\n", a << 1); // 运行结果(注意, 是按十六进制打印的) //20
【注意】:向左移位时,丢弃的是符号位,因此正数左移可能会编程负数
-
右移 >>: 最右侧位不要了, 最左侧补符号位(正数补0, 负数补1)
int a = 0x10;
System.out.printf("%x\n", a >> 1);
// 运行结果(注意, 是按十六进制打印的)
//8
int b = 0xffff0000;
System.out.printf("%x\n", b >> 1);
// 运行结果(注意, 是按十六进制打印的)
//ffff8000
-
无符号右移 >>>: 最右侧位不要了, 最左侧补 0.
int a = 0xffffffff; System.out.printf("%x\n", a >>> 1); // 运行结果(注意, 是按十六进制打印的) //7ffffff
【注意】:
- 左移 1 位, 相当于原数字 * 2. 左移 N 位, 相当于原数字 * 2 的N次方.
- 右移 1 位, 相当于原数字 / 2. 右移 N 位, 相当于原数字 / 2 的N次方.
- 由于计算机计算移位效率高于计算乘除, 当某个代码正好乘除 2 的N次方的时候可以用移位运算代替.
- 移动负数位或者移位位数过大都没有意义.
3.7 条件运算符
条件运算符只有一个: 表达式1 ? 表达式2 : 表达式3
当 表达式1 的值为true
时, 整个表达式的值为 表达式2
的值;
当 表达式1 的值为 fals
e 时, 整个表达式的值为 表达式3
的值.
也是 Java 中唯一的一个 三目运算符, 是条件判断语句的简化写法.
// 求两个整数的最大值
int a = 10;
int b = 20;
int max = a > b ? a : b;
【注意】:
-
表达式2和表达式3的结果要是同类型的,除非能发生类型隐式类型转换
int a = 10; int b = 20; int c = a > b? 1 : 2.0;
-
表达式不能单独存在,其产生的结果必须要被使用。
3.8 运算符的优先级
在一条表达式中,各个运算符可以混合起来进行运算,但是运算符的优先级不同,比如:*
和 /
的优先级要高于 +
和-
,有些情况下稍不注意,可能就会造成很大的麻烦。
// 求a和b的平均值
int a = 10;
int b = 20;
int c = a + (b - a) >> 1;
System.out.println(c);
上述表达式中,由于 +
的优先级要高于 >>
, 因此a
先和b-a
的结果做加法,整体为20,最后再进行右移,因此结果 为10。
【注意】:运算符之间是有优先级的. 具体的规则我们不必记忆. 在可能存在歧义的代码中加上括号即可.
4. 程序逻辑控制
4.1 顺序结构
顺序结构比较简单,按照代码书写的顺序一行一行执行。
public class Test {
public static void main(String[] args) {
System.out.println("aaa");
System.out.println("bbb");
System.out.println("***c");
/* 运行结果
aaa
bbb
***c
*/
}
}
如果调整代码的书写顺序, 则执行顺序也发生变化。
public class Test {
public static void main(String[] args) {
System.out.println("aaa");
System.out.println("***c");
System.out.println("bbb");
/* 运行结果
aaa
***c
bbb
*/
}
}
4.2 分支结构(选择结构)
它的作用是根据判断的条件是否成立(真或假),来决定后续代码执行顺序。
举例:
如同在岔路口做选择。不同的选择会带来不同的路径及结果。
4.2.1 if 语句
-
语法格式1
if (布尔表达式) { // 语句 }
如果布尔表达式结果为
true
,执行if
中的语句,否则不执行。执行流程:
举例:小明,如果这次考试考60分或以上就不挂科。
public class Test { public static void main(String[] args) { int score = 90; if (score >= 60) { System.out.println("不挂科"); } } }
-
语法格式2
if (布尔表达式) { // 语句 } else { // 语句 }
如果布尔表达式结果为
true
,执行if
中的语句,执行else
中的语句。执行流程:
举例:小明,如果这次考试考60分或以上就不挂科,没有考到60分就挂科。
public class Test { public static void main(String[] args) { int score = 90; if (score >= 60) { System.out.println("不挂科"); } else { System.out.println("挂科"); } } }
-
语法格式3
if(布尔表达式1){ // 语句1 }else if(布尔表达式2){ // 语句2 }else{ // 语句2 }
表达式1成立,执行语句1,否则表达式2成立,执行语句2,否则执行语句3
比如:考虑到学生自尊,不公开分数排名,因此:
- 分数在 [90, 100] 之间的,为优秀
- 分数在 [80, 90) 之前的,为良好 分数在 [70, 80) 之间的,为中等
- 分数在 [60, 70) 之间的,为及格 分数在 [ 0, 60) 之间的,为不及格
- 错误数据
按照上述办法通知学生成绩。
public class Test { public static void main(String[] args) { int score = 90; if(score >= 90){ System.out.println("优秀"); }else if(score >= 80 && score < 90){ System.out.println("良好"); }else if(score >= 70 && score < 80){ System.out.println("中等"); }else if(score >= 60 && score < 70){ System.out.println("及格"); }else if(score >= 0 && score < 60){ System.out.println("不及格"); }else{ System.out.println("错误数据"); } } }
【注意事项】:
-
代码风格
// 风格1-----> 推荐 int x = 10; if (x == 10) { // 语句1 } else { // 语句2 } // 风格2 int x = 10; if (x == 10) { // 语句1 } else { // 语句2 }
虽然两种方式都是合法的, 但是 Java 中更推荐使用风格1,代码跟紧凑。
-
分号问题
int x = 20; if (x == 10); { System.out.println("hehe"); } // 运行结果 hehe
此处多写了一个 分号, 导致分号成为了
if
语句的语句体, 而 { } 中的代码已经成为了和一个 if 无关的代码块,所以运行结果是haha
,而不是空白。 -
悬垂
else
问题int x = 10; int y = 10; if (x == 10) if (y == 10) System.out.println("aaa"); else System.out.println("bbb");
if else
语句中可以不加大括号 . 但是也可以写语句(只能写一条语句). 此时else
是和最接近的if
匹配. 但是实际开发中我们不建议这么写. 最好加上大括号.
4.2.2 switch 语句
基本语句
switch(表达式){
case 常量值1:{
语句1;
[break;]
}
case 常量值2:{
语句2;
[break;]
}
...
default:{
内容都不满足时执行语句;
[break;]
}
}
执行流程:
- 先计算表达式的值
- 和
case
依次比较,一旦有响应的匹配就执行该项下的语句,直到遇到break
时结束 - 当表达式的值没有与所列项匹配时,执行
default
代码示例:
public class Test {
public static void main(String[] args) {
String week = "周四";
switch (week) {
case "周一":
System.out.println("埋头苦干,写程序");
break;
case "周二":
System.out.println("请求学长帮忙解决bug");
break;
case "周三":
System.out.println("今晚烧烤、小龙虾");
break;
case "周四":
System.out.println("帮助学妹解决bug");
break;
case "周五":
System.out.println("今晚吃鸡");
break;
case "周六":
System.out.println("上GitHub交友");
break;
case "周日":
System.out.println("郁郁寡欢、准备上课");
default:
System.out.println("输入错误");
}
}
}
【注意事项】:
-
多个
case
后的常量值不可以重复 -
switch
的括号内只能是以下类型的表达式:- 基本类型:byte、char、short、int,注意不能是long类型
- 引用类型:String常量串、枚举类型
-
break
不要遗漏, 否则会失去 “多分支选择” 的效果int day = 1; switch(day) { case 1: System.out.println("星期一"); // break; case 2: System.out.println("星期二"); break; } // 运行结果 星期一 星期二
-
switch
不能表达复杂的条件// 例如: 如果 num 的值在 10 到 20 之间, 就打印 hehe // 这样的代码使用 if 很容易表达, 但是使用 switch 就无法表示. if (num > 10 && num < 20) { System.out.println("hehe"); }
-
switch
虽然支持嵌套, 但是很丑,一般不推荐public class Test { public static void main(String[] args) { int x = 1; int y = 1; switch(x) { case 1: switch(y) { case 1: System.out.println("hehe"); break; } break; case 2: System.out.println("haha"); break; } } }
综上, 我们发现, switch
的使用局限性是比较大的
4.3 循环结构
循环结构它是在满足条件的情况下,反复执行某一段代码的计算过程。
举例:
我们要围着操场跑 5 圈,跑圈这个行为就重复了 5 次,也就是循环了 5 次。
4.3.1 while 语句
基本格式:
while (循环条件) {
// 语句
}
执行流程:
首先执行判断表达式,表达式的值为false
,循环直接结束;表达式的值为true
,则执行循环语句,语句执行完后再继续判断,是否进行下一次判断。
代码示例:打印1~10的值
public class Test {
public static void main(String[] args) {
int i = 1;
while (i <= 10) {
System.out.println(i);
i++;
}
}
}
4.3.2 for 语句
基本格式:
for (表达式1;表达式2;表达式3) {
// 语句
}
表达式1:用于循环变量的初始化
表达式2:用于循环结束条件的判断
表达式3:用于循环变量的调整
执行流程:
首先执行表达式1
初始化循环变量,接下来就是执行表达式2
的判断部分,表达式2
的结果如果为false
,则循环结束;如果表达式2
的结果为true
,则执行循环语句,循环语句执行完后,再去执行表达式3
,调整循环变量,然后再去表达式2
的地方执行判断,表达式2
的结果是否为false
,决定循环是否继续。
整个循环的过程中,表达式1
初始化部分只被执行1次,剩下的就是表达式2
、循环语句、表达式3
在循环
代码示例:打印1~10的值
public class Test {
public static void main(String[] args) {
for (int i = 1; i <= 10; i++) {
System.out.println(i);
}
}
}
4.3.3 do…while循环
基本格式:
do{
语句
}while(表达式);
while
和 for
这两种循环都是先判断,条件如果满足就进入循环,执行循环语句,如果不满足就跳出循环;
而do...while
循环则是先直接进入循环体,执行循环内部,然后在执行 while
后的判断表达式,表达式为真,就会进行下一次,表达式为false
,则不再继续循环。
执行流程:
在do...while
循环中先执行图上的“语句”,执行完语句,在去执行“判断表达式”,在判断表达式的结果是true
,则继续循环,执行循环;判断表达式的结果false
,则循环结束。
所以在do...while
语句中循环体是至少执行一次的,这是do...while
循环比较特殊的地方。
代码示例:打印1~10的值
public class Test {
public static void main(String[] args) {
int i = 1;
do {
System.out.println(i);
i++;
} while (i <= 10);
}
}
4.3.4 while语句 和 for语句的对比
4.4 break和continue
在循环执行的过程中,如果某些状况发生的时候,需要提前终止循环。
-
break
的作用是用于永久的终止循环,只要break
被执行,直接就会跳出循环,继续往后执行。代码示例:找到100~200之间的第一个3的倍数
public class Test { public static void main(String[] args) { for (int num = 100; num <= 200; num++) { if (num % 3 == 0) { System.out.println(num); break; } } } } // 运行结果 102
-
continue
的作用是跳出本次循环continue
后边的代码代码示例:找到100~200中的所有3的倍数
public class Test { public static void main(String[] args) { for (int num = 100; num <= 200; num++) { if (num % 3 == 0){ System.out.println(num); continue; } } } }
5. 方法
5.0 前言
在编程的过程中,经常会出现一部分代码多次使用的情况,比如计算多边形面积,输出固定格式的文字等。
今天我们就来学习方法。
5.1 方法的概念和使用
5.1.1 什么是方法
方法就是一个代码片段,类似于C语言的“函数”。
方法存在的意义:
- 是能够模块化的组织代码(当代码规模比较复杂的时候)
- 做到代码被重复使用, 一份代码可以在多个位置使用.
- 让代码更好理解更简单.
- 直接调用现有方法开发, 不必重复造轮子
5.1.2 方法的定义
方法的语法格式:
修饰符 返回值类型 方法名称(形参列表){
// 方法体
return 返回值;
}
代码示例:实现一个两个整数相加的方法
public static int add(int a, int b) {
int c = a + b;
return c;
}
【注意事项】:
- 修饰符:现阶段直接使用
public static
固定搭配 - 返回值类型:如果方法有返回值,返回值类型必须要与返回的实体类型一致,如果没有返回值,必须写成
void
- 方法名字:采用小驼峰命名
- 参数列表:如果方法没有参数,()中什么都不写,如果有参数,需指定参数类型,多个参数之间使用逗号隔开
- 方法体:方法内部要执行的语句
- 在 Java当中,方法必须写在类当中
- 在 Java当中,方法不能嵌套定义
- 在 Java当中,没有方法声明一说
5.1.3 方法调用的过程
方法调用过程:
调用方法—>传递参数—>找到方法地址—>执行被调方法的方法体—>被调方法结束返回—>回到主调方法继续往下执行
【注意事项】
- 定义方法的时候, 不会执行方法的代码. 只有调用的时候才会执行.
- 一个方法可以被多次调用
代码示例:计算两个整数相加
public class Main {
public static void main(String[] args) {
int x = 10;
int y = 20;
int ret = add(x, y);
System.out.println(ret);
}
public static int add(int a, int b) {
int c = a + b;
return c;
}
}
方法调用过程:
DeBug
-
内存图
方法是放在方法区中的,被调用的时候,需要进入到栈内存中运行
一旦程序遇到
return
或者方法执行结束,就会把当前方法栈帧就从栈上进行销毁(回收)
5.1.4 形参和实参(重要)
方法的形参相当于数学函数中的自变量,比如:1 + 2 + 3 + … + n的公式为sum(n) =(1 + n) * n / 2
Java中方法的形参就相当于sum函数中的自变量n,用来接收sum函数在调用时传递的值的。形参的名字可以随意取,对方法都没有任何影响,形参只是方法在定义时需要借助的一个变量,用来保存方法在调用时传递过来的值。
public class Main {
public static void main(String[] args) {
getSum(10); // 10是实参,在方法调用时,形参n用来保存10
getSum(100); // 100是实参,在方法调用时,形参n用来保存100
}
private static int getSum(int n) { // n 是形参
return (1 + n) * n / 2;
}
}
再比如:
public class Main {
public static void main(String[] args) {
add(2,3); // 2 和 3 是实参,在调用时传给形参a 和 b
}
public static int add(int a, int b) {
return a + b;
}
}
5.1.5 没有返回值的方法
方法的返回值是可选的. 有些时候可以没有的,没有时返回值类型必须写成void
代码示例:
public class Main {
public static void main(String[] args) {
int a = 10;
int b = 20;
print(a,b);
}
private static void print(int x, int y) {
System.out.println("x = " + x + ",y = " + y);
}
}
return
问题
-
如果没有返回值的方法要写
return
,则return
后面不能加任何返回值。 -
return
后面的语句不会被执行,return
表示方法的结束
5.2 方法的重载
5.2.1 为什么需要方法的重载
由于参数类型不匹配, 所以不能直接使用现有的 add
方法.
一种比较简单粗暴的解决方法如下:
public class Main {
public static void main(String[] args) {
int x = 10;
int y = 20;
System.out.println(addInt(x,y));
double a = 1.1;
double b = 1.2;
System.out.println(addDouble(a,b));
}
public static int addInt(int a, int b) {
return a + b;
}
public static double addDouble(double a, double b) {
return a + b;
}
}
上述代码确实可以解决问题,但不友好的地方是:需要提供许多不同的方法名,而取名字本来就是让人头疼的事情。那能否将所有的名字都给成 add
呢?
5.2.2 方法重载的概念
在Java中,如果多个方法的名字相同,参数列表不同,则称该几种方法被重载了。
public class Main {
public static void main(String[] args) {
int x = 10;
int y = 20;
System.out.println(add(x,y));// 调用add(int, int)
double a = 1.1;
double b = 1.2;
System.out.println(add(a,b));// 调用add(double, double)
double c = 1.3;
System.out.println(add(a,b,c));// 调用add(double, double,double)
}
public static int add(int a, int b) {
return a + b;
}
public static double add(double a, double b) {
return a + b;
}
public static double add(double a, double b, double c) {
return a + c;
}
}
注意:
-
方法名必须相同
-
参数列表必须不同(参数的个数不同、参数的类型不同、类型的次序必须不同)
-
与返回值类型是否相同无关
5.2.3 方法签名
在同一个作用域中不能定义两个相同名称的标识符。比如:方法中不能定义两个名字一样的变量,那为什么类中就可以定义方法名相同的方法呢?
方法签名即:经过编译器编译修改过之后方法最终的名字。具体方式:方法全路径名+参数列表+返回值类型,构成方法完整的名字。
5.3 递归
5.3.1 生活中的例子
从前有坐山,山上有座庙,庙里有个老和尚给小和尚将故事,讲的就是:
"从前有座山,山上有座庙,庙里有个老和尚给小和尚讲故事,讲的就是:
“从前有座山,山上有座庙…”
“从前…”
上面的两个例子有个共同的特征:自身中又包含了自己,该种思想在数学和编程中非常有用,因为有些时候,我们遇到的问题直接并不好解决,但是发现将原问题拆分成其子问题之后,子问题与原问题有相同的解法,等子问题解决之后,原问题就迎刃而解了。
5.3.2 递归的概念
一个方法在执行过程中调用自身, 就称为 “递归”.
递归相当于数学上的 “数学归纳法”, 有一个起始条件, 然后有一个递推公式.
例如, 我们求 N!
起始条件: N = 1 的时候, N! 为 1. 这个起始条件相当于递归的结束条件.
递归公式: 求 N! , 直接不好求, 可以把问题转换成 N! => N * (N-1)!
递归的必要条件:
- 将原问题划分成其子问题,注意:子问题必须要与原问题的解法相同
- 递归出口
代码示例
public class Main {
public static void main(String[] args) {
fun();
}
public static void fun() {
fun();
}
}
上述代码就是一个最简单的递归。
但是存在错误:出来栈溢出错误的时候,就说明结束条件不对或者没有结束条件
代码示例:递归求 N 的阶乘
public class Main {
public static void main(String[] args) {
System.out.println(factor(5)); // 120
}
public static int factor(int n) {
if (n == 1) {
return 1;
}
return factor(n - 1) * n; // factor()方法调用自己
}
}
5.3.3 递归执行过程分析
递归的程序的执行过程不太容易理解, 要想理解清楚递归, 必须先理解清楚 “方法的执行过程”, 尤其是 "方法执行结束之后, 回到调用位置继续往下执行.
代码示例:递归求 N 的阶乘
public class Main {
public static void main(String[] args) {
System.out.println(factor(5));
}
public static int factor(int n) {
System.out.println("函数开始, n = " + n);
if (n == 1) {
System.out.println("函数结束, n = 1 ret = 1");
return 1;
}
int ret = n * factor(n - 1);
System.out.println("函数结束, n = " + n + " ret = " + ret);
return ret;
}
}
/*
函数开始, n = 5
函数开始, n = 4
函数开始, n = 3
函数开始, n = 2
函数开始, n = 1
函数结束, n = 1 ret = 1
函数结束, n = 2 ret = 2
函数结束, n = 3 ret = 6
函数结束, n = 4 ret = 24
函数结束, n = 5 ret = 120
120
*/
执行图:
6. 数组
6.1 数组的基本概念
6.1.1 为什么使用数组?
假设现在要存储5个学生的年龄,按照之前掌握的知识点,我们会写出如下代码:声明5个变量存储学生变量
public class Test {
public static void main(String[] args) {
int age1;
int age2;
int age3;
int age4;
int age5;
}
}
如果我们有10个学生呢?我们就要声明20个变量,似乎没有什么问题。那如果有100,1000个学生呢,我们就要声明100,1000个变量,这样就有点离谱了,使用数组我们就可以解决一个问题。
6.1.2 什么是数组
数组,是指一组类型相同的数据的集合,数组中每个数据称为元素。数组可以存放任意类型的元素,但同一个数组里存放的元素类型必须一致。数组分为一维数组和多维数组。
数组在内存中是一段连续的空间,比如现实中的车库:
在 Java中,包含6个整形类型元素的数组,就相当于上图中连在一起的6个车位,从上图中可以看到:
-
数组中存放的元素其类型相同
-
数组的空间是连在一起的
-
每个空间有自己的编号,起始位置的编号为0,即数组的下标。
6.1.3 数组的创建和初始化
6.1.3.1 数组的创建
基本语法格式:
T[] 数组名 = new T[N];
-
T
:表示数组中存放元素的类型 -
T[]
:表示数组类型 -
N
:表示数组的长度
代码示例:存储10个人的年龄
int[] ages = new int[10];
6.1.3.2 数组的初始化
Java 数组初始化主要分为静态初始化以及动态初始化
-
动态初始化:在创建数组时,直接指定数组中元素的个数
int[] ages = new int[10];
-
动态初始化:在创建数组是不直接指定数据元素个数,而直接讲具体的数据内容进行指定
语法格式:
T[] 数组名 = {data1,data2,....data};
int[] ages = new {1,2,3,4,5};
【注意事项】
-
静态初始化虽然没有指定数组的长度,编译器在编译时会根据{}中元素个数来确定数组的长度。
-
静态初始化时, {}中数据类型必须与[]前数据类型一致。
-
静态初始化可以简写,省去后面的new T[]。
int[] arr = {1,3,2,5,4}; // 注意:虽然省去了new T[], 但是编译器编译代码时还是会还原
-
数组也可以按照如下C语言个数创建,不推荐
int arr[] = {1, 2, 3}; /* 该种定义方式不太友好,容易造成数组的类型就是int的误解 []如果在类型之后,就表示数组类型,因此int[]结合在一块写意思更清晰 */
-
静态和动态初始化也可以分为两步,但是省略格式不可以。
public class Main { public static void main(String[] args) { int[] array1; array1 = new int[10]; int[] array2; array2 = new int[]{10, 20, 30}; // 注意省略格式不可以拆分, 否则编译失败 //int[] array3; //array3 = {1, 2, 3}; } }
-
如果没有对数组进行初始化,数组中元素有其默认值
-
如果数组中存储元素类型为基类类型,默认值为基类类型对应的默认值,比如:
类型 默认值 byte 0 short 0 int 0 long 0 float 0.0f double 0.0 char /u0000 boolean false -
如果数组中存储元素类型为引用类型,默认值为
null
-
6.1.4 数组的使用
6.1.4.1 数组中元素访问
数组在内存中是一段连续的空间,空间的编号都是从0开始的,依次递增,该编号称为数组的下标,数组可以通过下标访问其任意位置的元素。比如:
public class Main {
public static void main(String[] args) {
int[] arr = new int[]{1,2,3,4,5};
System.out.println(arr[0]);
System.out.println(arr[1]);
System.out.println(arr[2]);
System.out.println(arr[3]);
System.out.println(arr[4]);
}
}
【注意事项】:
-
数组是一段连续的内存空间,因此支持随机访问,即通过下标快速访问数组中任意位置的元素
-
下标从0开始,介于[0,N) 之间不包含N,N为元素个数,不能越界,否则会报出下标越界异常。
抛出了
java.lang.ArrayIndexOutOfBoundsException
异常. 使用数组一定要下标谨防越界.
6.1.4.2 遍历数组
所谓 “遍历” 是指将数组中的所有元素都访问一遍, 访问是指对数组中的元素进行某种操作,比如:打印。
public class Main {
public static void main(String[] args) {
int[] arr = new int[]{1,2,3,4,5};
System.out.println(arr[0]);
System.out.println(arr[1]);
System.out.println(arr[2]);
System.out.println(arr[3]);
System.out.println(arr[4]);
}
}
上述代码可以起到对数组中元素遍历的目的,但问题是:
-
如果数组中增加了一个元素,就需要增加一条打印语句
-
如果输入中有100个元素,就需要写100个打印语句
-
如果现在要把打印修改为给数组中每个元素加1,修改起来非常麻烦。
通过观察代码可以发现,对数组中每个元素的操作都是相同的,则可以使用循环来进行打印。
1. 循环遍历数组
public class Main {
public static void main(String[] args) {
int[] arr = new int[]{1,2,3,4,5};
for (int i = 0; i < 5; i++) {
System.out.println(arr[i]);
}
}
}
改成循环之后,上述三个缺陷可以全部2和3问题可以全部解决,但是无法解决问题1。那能否获取到数组的长度呢?
【注意】:在数组中可以通过 数组对象.length
来获取数组的长度
public class Main {
public static void main(String[] args) {
int[] arr = new int[]{1,2,3,4,5};
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
}
2. 使用 for-each
遍历数组
语法格式:
public class Main {
public static void main(String[] args) {
int[] arr = new int[]{1,2,3,4,5};
for (int x : arr) {
System.out.println(x);
}
}
}
for-each
是 for
循环的另外一种使用方式. 能够更方便的完成对数组的遍历. 可以避免循环条件和更新语句写错.
for-each
循环语句的循环变量将会遍历数组中的每个元素,而不是下标值。
3. 数组转字符串输出
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
int[] arr = new int[]{1,2,3,4,5};
String ret = Arrays.toString(arr);
System.out.println(ret);
}
}
代码分析:
6.2 数组是引用类型
6.2.1 JVM 内存分布
内存是一段连续的存储空间,主要是用来存储程序运行时数据的。比如:
- 程序运行时代码需要加载到内存
- 程序运行产生的中间数据要存放在内存
- 程序中的常量也要保存
- 有些数据可能需要长时间存储,而有些数据当方法运行结束后就要被销毁。
如果对内存中存储的数据不加区分的随意存储,那对内存管理起来将会非常麻烦。比如:
因此 JVM 也对所使用的内存按照功能的不同进行了划分:
- 程序计数器:只是一个很小的空间,保存下一条执行的指令的地址
- 虚拟机栈:与方法调用相关的一些信息,每个方法在执行时,都会先创建栈帧,栈帧中包含有:局部变量表、操作数栈、动态链接、返回地址以及其他的一些信息,保存的都是与方法执行时相关的一些信息。比如:局部变量。当方法运行结束后吧,栈帧就被销毁了,即栈帧中保存的数据也被销毁了。
- 本地方法栈:本地方法栈于虚拟机栈的作用类似,只不过保存的内容是方法的局部变量。在有些版本的 JVM 实现中,本地方法栈和虚拟机栈是一起的
- 堆:JVM 所管理的最大内存区域,使用**
new
创建的对象都是在堆上保存,堆是随着程序开始运行时而创建,随着程序的结束而销毁,堆中的数据只要还有在使用,就不会被销毁** - 方法区:用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法编译出的字节码就是保存在这个区域。
6.2.2 基本类型的变量与引用类型变量的区别
基本数据类型的变量,称为基本变量,该变量空间中直接存放的是其所对应的值;
而引用数据类型创建的变量,一般称为对象的引用,其空间中存储的是对象所在空间的地址
public class Main {
public static void main(String[] args) {
int a = 10;
int[] arr = new int[]{1,2,3};
}
}
在上述代码中,a
、arr
,都是函数内部的变量,因此其空间都在main
方法对应的栈帧中分配。a
是内置类型的变量,因此其空间中保存的就是给该变量初始化的值。arr
是数组类型的引用变量,其内部保存的内容可以简单理解成是数组在堆空间中的首地址。
上图可以看出,引用变量并不直接存储对象本生,可以简单理解成存储的是对象在堆中空间的起始地址。通过该地址,引用变量便可以去操作对象。有点类似C语言中的指针,但是 Java 中引用要比指针的操作更简单。
6.2.3 引用变量
public class Main {
public static void main(String[] args) {
int[] arr1 = new int[3];
arr1[0] = 1;
arr1[1] = 2;
arr1[2] = 3;
int[] arr2 = new int[]{1,2,3,4,5};
arr2[0] = 100;
arr2[1] = 200;
arr1 = arr2;
arr1[2] = 300;
arr1[3] = 400;
arr2[4] = 500;
for (int x : arr1) {
System.out.println(x);
}
}
}
6.2.4 认识 null
null
在 Java 中表示“空引用”,也就是一个不指向对象的引用
public class Main {
public static void main(String[] args) {
int[] arr = null;
System.out.println(arr[0]);
}
}
null
的作用类似于C语言中的NULL
(空指针),都是表示一个无效的内存位置。因此不能对这个内存进行任何读写操作。一旦尝试读写,就会抛出NullPointerException
【注意】:Java 中并没有约定
null
和 0 下标地址的内存有任何关联。
6.3 数组应用场景
6.3.1 保存数据
public class Main {
public static void main(String[] args) {
int[] arr = new int[]{1,2,3};
for (int x : arr) {
System.out.println(x);
}
}
}
6.3.2 作为方法的参数
-
参数传基本数据类型
public class Main { public static void main(String[] args) { int num = 0; func(num); System.out.println("num = " + num);// 0 } private static void func(int x) { x = 10; System.out.println("x = " + x); // 10 } }
上述代码我们可以发现
func
方法中修改了形参x
的值,不影响实参的num
值。 -
参数传引用数据类型
public class Main { public static void main(String[] args) { int[] arr = new int[]{1,2,3}; fun1(arr); System.out.println(Arrays.toString(arr)); // [1,2,3] fun2(arr); System.out.println(Arrays.toString(arr)); // [99,2,3] } public static void fun1(int[] arr) { arr = new int[]{11,22,33,44,55}; // 修改了形参的指向 } public static void fun2(int[] arr) { arr[0] = 99; // 形参改变了实惨的值 } }
上述代码我们可以发现
fun1
方法中修改了形参的指向,不影响实参数组的值fun2
方法内部修改了数组的内容,方法外部的数组内容也发生了改变。因为数组是引用类型,按照引用类型进行传递,是可以修改其中存放的内容的。
【总结】:所谓的“引用”本质只是存了地址。Java 将数组设定为引用类型,这样的话后续进行数组参数传参,其实只是将数组的地址传入函数形参中,这样可以避免对整数数组的拷贝(数组可能比较长,那么拷贝开销就会很大)。
6.3.3 作为方法的返回值
public class Main {
public static void main(String[] args) {
int[] ret = fun();
System.out.println(Arrays.toString(ret)); // [1, 2, 3, 4, 5]
}
public static int[] fun() {
int[] arr = new int[]{1,2,3,4,5};
return arr;
}
}
6.4 二维数组
二维数组本质上也就是一维数组,只不过每个元素又是一个一维数组
基本语法:
数据类型[][] 数组名称 = new 数据类型 [行数][列数] { 初始化数据 };
代码示例:
public class Main {
public static void main(String[] args) {
int[][] arr = {{1, 2, 3},{4,5,6}};
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr[i].length; j++) {
System.out.print(arr[i][j] + " ");
}
System.out.println();
}
System.out.println("=======");
for (int[] tempArr : arr) {
for (int x : tempArr) {
System.out.print(x + " ");
}
System.out.println();
}
System.out.println("=======");
String ret = Arrays.deepToString(arr); // deepToString()深度打印
System.out.println(ret);
}
}
Java 二维数组在定义的时候是可以省略列的
int[][] arr = new int[2][];
二维数组的用法和一维数组并没有明显差别, 因此我们不再赘述.
同理, 还存在 “三维数组”, “四维数组” 等更复杂的数组, 只不过出现频率都很低.
6.5 不规则数组
代码示例:
public class Main {
public static void main(String[] args) {
int[][] arr = new int[2][];
// 每一个一维数组 进行初始化
arr[0] = new int[3];
arr[1] = new int[5];
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr[i].length; j++) {
System.out.print(arr[i][j] + " ");
}
System.out.println();
}
}
}
// 运行结果
0 0 0
0 0 0 0 0
7. 类和对象
7.1 面向对象程序设计概述
面向对象程序设计(object-oriented programming,OOP),是当今主流的程序设计范畴,它取代了20世纪70年代的”结构化“或过程式编程技术。
面向对象的程序是由对象组成的,每个对象包含对用户公开的特点功能部分和隐藏的实现部分。
7.1.1 类
类(class)是构造对象的模板或蓝图。由类构造(construct)对象的过程称为类的实例(instance)。
封装(encapsulation,有时称为数据隐藏)是处理对象的一个重要概念。封装就是将数据和行为组合在一个包中,并对对象的使用者隐藏具体的实现方式。对象的数据称为实例字段(instance field),操作数据的过程称为方法(method)。
实现封装的关键在于,绝对不能让类中的方法直接访问其他类的实例字段。程序只能通过对象的方法与对象数据进行交互。封装给对象赋予了“黑盒”特征,这是提高重用性和可靠性的关键。这意味着一个类可以完全改变存储数据的方式,只要仍旧使用同样的方法操作数据,其他对象就不会知道也不用关心这个类所发生的变化。
7.1.2 对象
要想使用OOP,一定要清楚对象的三个主要特性:
- 对象的行为(behavior)——可以对对象完成哪些操作,或者可以对对象应用哪些方法?
- 对象的状态(state)—当调用那些方法时,对象会如何响应?
- 对象的标识(identity)——如何区分具有相同行为与状态的不同对象?
同一个类的所有对象实例,由于支持相同的行为而具有家族式的相似性。对象的行为是可调用的方法来定义的。
7.1.3 类之间的关系
在类之间,最常见的关系有:
- 依赖(uses-a);
- 聚合(has-a);
- 继承(is-a)。
依赖(dependence),即“uses-a”关系,是一种最明显、最常见的关系。如果一个类的方法使用或操纵另一个类的对象,我们就说一个类依赖于另一个类。
聚合(aggregation),即“has-a”关系,很容易理解,因为这种关系很具体。包含关系意味着类A的对象包含类B的对象。
继承(inheritance),即“is-a”关系,表示一个更特殊的类与一个更一般类之间的关系。
7.2 类的定义和使用
面向对象程序设计关注的是对象,而对象是现实生活中的实体。
7.2.1 简单认识类
类是用来对一个实体(对象)来进行描述,主要描述该实体(对象)具有哪些属性,哪些功能,描述完成后计算机就可以识别了。
7.2.2 类的定义格式
在java
中定义 类需要使用class
关键字,具体语法如下:
class ClassName{
field;// 字段 或 成员变量
method;// 方法 或 成员方法
}
class
为定义类的关键字,ClassName
为类的名字,{}
中为类的主体。
类中包含的内容称为类的成员。属性主要是用来描述类的,称之为类的成员属性或者类**成员变量**。方法主要说明类具有哪些功能,称为类的成员方法。
7.2.3 自定义类
7.2.3.1 定义一个狗类
class Dog {
// 狗的属性
public String name;// 狗的名字
public String color;// 狗的颜色
// 狗的行为
public void barks() {
System.out.println(name + "在旺旺叫");
}
public void wag() {
System.out.println(name + "在摇尾巴");
}
}
注意事项:
- 一般一个文件当中只定义一个类。
-
main
方法所在的类一般要使用public
修饰。 -
public
修饰的类必须要和文件相同。
7.3 类的实例化
7.3.1 什么是实例化
定义一个类,就相当于在计算机中定义了一种新的类型,与int
,double
类似,只不过int
和double
是java
语言自带的内置类型,而类是用户自定义了一个新的类型,比如上述的Dog
类。它就是类(一种新定义的类型)有了这些自定义的类型之后,就可以使用这些类来定义实例(或者称为对象)。
用类类型创建对象的过程,称为类的实例化,在java
采用new
关键字,配合类名来实例化对象。
public class Test {
public static void main(String[] args) {
Dog dog1 = new Dog();
dog1.name = "大黄";
dog1.color = "黄色";
dog1.barks();
dog1.wag();
Dog dog2 = new Dog();
dog2.name = "哈士奇";
dog2.color = "白黑色";
dog2.barks();
dog2.wag();
}
}
// 运行结果:
大黄在旺旺叫
大黄在摇尾巴
哈士奇在旺旺叫
哈士奇在摇尾巴
注意事项:
-
new
关键字用于创建一个对象的实例。 - 使用
.
来访问对象中属性和方法。 - 同一个类可以创建多个实例。
7.3.2 类和对象的说明
- 类只是一个模型一样的东西,用来对一个实体进行描述,限定了类有哪些成员。
- 类是一种自定义的类型,可以用来定义常量
- 一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量
- 做个比方,类的实例化出对象就像现实中使用建筑设计圈造出房子,类就是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化的对象才能实际存储数据,占用物理空间。
7.4 this 引用
7.4.1 为什么要使用this引用
先看一个日期类的例子:
class Date{
public int year;
public int month;
public int day;
public void setDate(int y, int m, int d){
year = y;
month = m;
day = d;
}
public void printDate(){
System.out.println(year + "/" + month + "/" + day);
}
}
public class Test2 {
public static void main(String[] args) {
// 构造三个日期类型的对象 d1 d2 d3
Date d1 = new Date();
Date d2 = new Date();
Date d3 = new Date();
// 对d1,d2,d3的日期设置
d1.setDay(2020,9,15);
d2.setDay(2020,9,16);
d3.setDay(2020,9,17);
// 打印日期中的内容
d1.printDate();
d2.printDate();
d3.printDate();
}
}
以上代码定义了一个日期类,然后main方法中创建了三个对象,并通过Date类中的成员方法对对象进行设置和打
印,代码整体逻辑非常简单,没有任何问题。
但是细思之下有以下两个疑问:
-
形参名不小心与成员变量名相同
public void setData(int year, int month, int day){ year = year; month = month; day = day; }
那函数体中到底是谁给谁赋值?成员变量给成员变量?参数给参数?参数给成员变量?成员变量参数?估计
自己都搞不清楚了。 -
三个对象都在调用
setDate
和printDate
函数,但是这两个函数中没有任何有关对象的说明,setDate
和printDate
函数如何知道打印的是那个对象的数据呢?
7.4.2 什么是this引用
this引用指向当前对象(成员方法运行时调用该成员方法的对象),在成员方法中所有成员变量的操作,都是通过该引用去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
public void setData(int year, int month, int day){
this.year = year;
this.month = month;
this.day = day;
}
注意:this引用的是调用成员方法的对象。
public class Test {
public static void main(String[] args) {
Date d = new Date();
d.setData(2020,9,15);
d.printDate();
}
}
7.4.3 this引用的特性
-
this
的类型:对应类类型引用,即哪个对象调用就是哪个对象的引用类型。 -
this
只能在“成员方法”中使用。 - 在"成员方法"中,
this
只能引用当前对象,不能再引用其他对象。 -
this
是“成员方法”第一个隐藏的参数,编译器会自动传递,在成员方法执行时,编译器负责将调用成员方法对象的引用传递给该成员方法,this
负责来接收。
7.5 对象的构造及初始化
7.5.1 如何初始化对象
在java
方法内部定义一个局部变量,必须初始化,否则就会编译失败。
要上诉代码编译成功,只需要是在使用a
之前,给a
设置一个初始值。如果是对象:
public class Test {
public static void main(String[] args) {
Date d = new Date();
d.setData(2020,9,15);
d.printDate();
}
}
// 代码正常通过编译
需要调用之前写的setDate
方法才可以将具体的日期设置到对象中。通过上述例子发现两个问题:
- 每次对象创建好后调用
setDate
方法设置具体日期,比较麻烦,那对象该如何初始化? - 局部变量必须要初始化才能使用,为什么字段声明之后没有给值依然可以使用?
7.5.2 构造方法
7.5.2.1 概念
构造方法(也称为构造器)是一种特殊的成员方法,名字必须与类名相同,在创建对象时,编译器自动调用并且在整个对象的生命周期内调用一次。
class Date{
public int year;
public int month;
public int day;
// 构造方法
public Date(int year, int month, int day){
this.year = year;
this.month = month;
this.day = day;
System.out.println("Date(int year, int month, int day)被调用了");
}
public void printDate(){
System.out.println(this.year + "/" +this.month + "/" + this.day);
}
}
public class Test {
public static void main(String[] args) {
Date d = new Date(2020,9,15);
d.printDate();
}
}
注意:构造方法的作用就是对对象中的成员进行初始化,并不负责给对象开辟空间。
7.5.2.2 特性
- 构造器与类同名
- 每一个类可以有一个以上的构造器
- 构造器可以有0个、1个或多个参数
- 构造器没有返回值
- 构造器总是伴随着
new
操作符一起调用 - 构造方法可以重载
class Date{
public int year;
public int month;
public int day;
// 构造方法
// 无参构造方法
public Date() {
}
// 带3个参数的构造方法
public Date(int year, int month, int day) {
this.year = year;
this.month = month;
this.day = day;
}
public void printDate(){
System.out.println(this.year + "/" +this.month + "/" + this.day);
}
}
public class Test {
public static void main(String[] args) {
Date d = new Date(2020,9,15);
d.printDate();
}
}
// 上述两个构造方法:名字相同,参数列表不同,因此构成了方法重载。
-
如果用户没有显式定义,编译器会生成一份默认的构造方法,生成的默认构造方法一定是无参的
class Date{ public int year; public int month; public int day; public void printDate(){ System.out.println(this.year + "/" +this.month + "/" + this.day); } } public class Test { public static void main(String[] args) { Date d = new Date(); d.printDate(); } }
上述
Date
类,没有定义任何构造方法,编译器会默认生成一个无参构造器。注意:一旦用户定义,编译器就不会生成
-
构造方法中,可以通过this调用其他构造方法来简化代码
class Date{ public int year; public int month; public int day; public Date() { this(2005,5,9);// 必须是构造方法的第一条语句 } public Date(int year, int month, int day) { this.year = year; this.month = month; this.day = day; } public void printDate(){ System.out.println(this.year + "/" +this.month + "/" + this.day); } } public class Test2 { public static void main(String[] args) { Date d = new Date(); d.printDate(); } }
注意:this(…)必须是构造方法的第一条语句。
7.5.3 默认初始化
在上文中提出的第二个问题:为什么局部变量在使用时必须要初始化,而成员变量可以不用呢?
class Date{
public int year;
public int month;
public int day;
public Date(int year, int month, int day) {
// 成员变量在定义时,并没有给初始值, 为什么就可以使用呢?
System.out.println(this.year);
System.out.println(this.month);
System.out.println(this.day);
}
public void printDate(){
System.out.println(this.year + "/" +this.month + "/" + this.day);
}
}
public class Test2 {
public static void main(String[] args) {
Date d = new Date(2023,9,17);
}
}
要搞清楚这个过程,就需要知道new
关键字背后所发生的一些事情:
Date d = new Date(2023,9,17);
在程序层面只是简单的一条语句,在JVM层面需要做好多事情,下面简单介绍一下:
-
检测对象对应的类是否加载了,如果没有加载则加载
-
为对象分配内存空间
-
处理并发安全问题
比如:多个线程同时申请对象,
JVM
要保证给对象分配的空间不冲突 -
初始化所分配的空间
即:对象空间被申请好之后,对象中包含的成员已经设置好了初始值,比如:
数据类型 默认值 byte 0 char ‘\u0000’ short 0 int 0 long 0L boolean false float 0.0f double 0.0 reference null -
设置对象头信息
-
调用构造方法,给对象中各个成员赋值
7.5.4 就地初始化
在声明成员变量的时候,就可以给出初始值。
class Date{
public int year = 2021;
public int month = 5;
public int day = 19;
public Date() {
}
public Date(int year, int month, int day) {
this.year = year;
this.month = month;
this.day = day;
}
public void printDate(){
System.out.println(this.year + "/" +this.month + "/" + this.day);
}
}
public class Test2 {
public static void main(String[] args) {
Date d = new Date(2023,9,17);
d.printDate();
Date d1 = new Date();
d1.printDate();
}
}
// 运行结果
// 2023/9/17
// 2021/5/19
注意:代码编译完成后,编译器会将所有给成员初始化的这些语句添加到各个构造函数中。
8. 封装、继承和多态
面向对象三大特性:封装、继承和多态。
8.1 封装
8.1.1 封装的概念
在面向对象程式设计方法中,封装(英语:Encapsulation)是指一种将抽象性函式接口的实现细节部份包装、隐藏起来的方法。
封装可以被认为是一个保护屏障,防止该类的代码和数据被外部类定义的代码随机访问。
要访问该类的代码和数据,必须通过严格的接口控制。
封装最主要的功能在于我们能修改自己的实现代码,而不用修改那些调用我们代码的程序片段。
适当的封装可以让程式码更容易理解与维护,也加强了程式码的安全性。
8.1.2 为什么封装
封装的目的是保护数据的安全和完整性,同时隐藏数据的实现细节,提高代码的可维护性和可扩展性,具体有以下几个方面的好处:
- 良好的封装能够减少耦合。
- 类内部的结构可以自由修改。
- 可以对成员变量进行更精确的控制。
- 隐藏信息,实现细节。
8.1.3 封装的实现步骤
-
修改属性的可见性来限制属性的访问(用
private
来修饰),如:public class Test { private String name; private int age; }
这段代码中,将
name
和age
属性设置为私有的,只有在本类中被访问,其他类访问不了,就实现对信息的隐藏。 -
对每个值属性提供对外的公共方法访问,也就是创建一对赋取值方法,用于对私有属性的访问,例如:
public class Test { private String name; private int age; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } }
采用
this
关键字是为了解决实例变量和局部变量之间发生的同名的冲突。
8.2 继承
8.2.1 继承的概念
继承是 java
面向对象编程技术的一块基石,因为它允许创建分等级层次的类。
继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。
生活中的继承
兔子和羊属于食草动物,狮子和老虎属于食肉动物。
食草动物和食肉动物又是属于动物。
所有的继承需要符合的关系:is-a,父类更通用,子类更具体。
虽然食草动物和食肉动物都是属于动物,但是两者的属性和行为上有差别,所以子类会具有父类的一般特性也会具有自身的特性。
8.2.2 继承的格式
在 Java
中通过关键字 extends
来声明一个类是从另一个类继承而来的,一般格式如下:
class 父类 {
}
class 子类 extends 父类 {
}
8.2.3 为什么继承
接下来我们通过实例的说明这个需求。
开发动物类,其中动物分别为狗和猫,要求如下:
- 狗:属性(姓名,颜色),方法(吃,叫)
- 猫:属性(姓名,颜色),方法(吃,叫)
class Dog{
private String name;
private String color;
public void eat() {
System.out.println(this.name + "正在吃...");
}
public void cry() {
System.out.println(this.name + "正在叫...");
}
}
class Cat {
private String name;
private String color;
public void eat() {
System.out.println(this.name + "正在吃...");
}
public void cry() {
System.out.println(this.name + "正在叫...");
}
}
从这两段代码可以看出来,代码存在重复了,导致后果就是代码量大且臃肿,而且维护性不高(维护性主要是后期需要修改的时候,就需要修改很多的代码,容易出错),所以要从根本上解决这两段代码的问题,就需要继承,将两段代码中相同的部分提取出来组成 一个父类:
class Animal {
private String name;
private String color;
public Animal(String name, String color) {
this.name = name;
this.color = color;
}
public void eat() {
System.out.println(this.name + "正在吃...");
}
public void cry() {
System.out.println(this.name + "正在叫...");
}
}
这个Animal类就可以作为一个父类,然后狗类和猫类继承这个类之后,就具有父类当中的属性和方法,子类就不会存在重复的代码,维护性也提高,代码也更加简洁,提高代码的复用性(复用性主要是可以多次使用,不用再多次写同样的代码) 继承之后的代码:
- 狗类
class Dog extends Animal{
public Dog(String name , String color) {
super(name, color);
}
}
- 猫类
class Cat extends Animal {
public Cat(String name , String color) {
super(name, color);
}
}
8.2.4 继承类型
Java
中不支持多继承,但支持多重继承。
8.2.5 继承特性
- 子类拥有父类非
private
的属性、方法。 - 子类可以拥有自己的属性和方法,即子类可以对父类进行扩展。
- 子类可以用自己的方式实现父类的方法。
- Java 的继承是单继承,但是可以多重继承,单继承就是一个子类只能继承一个父类,多重继承就是,例如 A 类继承 B 类,B 类继承 C 类,所以按照关系就是 C 类是 B 类的父类,B 类是 A 类的父类,这是 Java 继承区别于 C++ 继承的一个特性。
- 提高了类之间的耦合性(继承的缺点,耦合度高就会造成代码之间的联系越紧密,代码独立性越差)。
8.2.6 super 与 this 关键字
super
关键字:我们可以通过super
关键字来实现对父类成员的访问,用来引用当前对象的父类
this
关键字:指向自己的引用。
class Animal {
void eat() {
System.out.println("animal : eat");
}
}
class Dog extends Animal {
void eat() {
System.out.println("dog : eat");
}
void eatTest() {
this.eat(); // this 调用自己的方法
super.eat(); // super 调用父类方法
}
}
public class Test {
public static void main(String[] args) {
Animal a = new Animal();
a.eat();
Dog d = new Dog();
d.eatTest();
}
}
// 运行结果
//animal : eat
//dog : eat
//animal : eat
8.2.7 final 关键字
final
关键字声明类可以把类定义为不能继承的,即最终类;或者用于修饰方法,该方法不能被子类重写;
- 声明类
final class 类名 {
// 类体
}
- 声明方法
访问限定符 final 返回值类型 方法名(){
// 方法体
}
注意:实例变量也可以被定义为final
,被定义为final
的变量不能被修改。被声明为final
的类的方法自动声明为final
,但是实例变量并不是final
。
8.3 多态
8.3.1 多态的概念
多态是同一个行为具有多个不同表现形式或形态的能力。
多态就是同一个接口,使用不同的实例而执行不同操作,如图所示:
多态性是对象多种表现形式的体现。
同一个事件发生在不同的对象上会产生不同的结果。
8.3.2 多态的优点
- 消除类型之间的耦合关系
- 可替换性
- 可扩充性
- 接口性
- 灵活性
- 简化性
8.3.3 多态存在的三个必要条件
-
继承
-
重写
重写:子类对父类的允许访问的方法的实现过程进行重新编写,返回值和形参都不能改变。即外壳不变,核心重写。
重写的好处:在于子类可以根据需要,定义特定于自己的行为。也就是说子类能够根据需要实现父类的方法。
class Animal { public void eat() { System.out.println("正在吃..."); } } class Dog extends Animal { public void eat() { System.out.println("狗正在吃狗粮..."); } } class Bird extends Animal { public void eat() { System.out.println("鸟正在吃鸟粮..."); } } public class Test1 { public static void fun(Animal animal) { animal.eat(); } public static void main(String[] args) { Dog dog = new Dog(); fun(dog); Bird bird = new Bird(); fun(bird); } } // 运行结果 狗正在吃狗粮... 鸟正在吃鸟粮...
重写(覆盖)的规则:
- 方法名相同
- 参数列表相同【顺序、个数、类型】
- 返回值相同
-
父类引用指向子类对象
比如:
Animal dog = new Dog();
当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,再去调用子类的同名方法。
多态的好处:可以使程序有良好的扩展,并可以对所有类的对象进行通用处理。
以下是多态的例子:
abstract class Animal {
abstract void eat();
}
class Cat extends Animal {
public void eat() {
System.out.println("吃鱼");
}
public void work() {
System.out.println("抓老鼠");
}
}
class Dog extends Animal {
public void eat() {
System.out.println("吃骨头");
}
public void work() {
System.out.println("看家");
}
}
// 测试类
public class Test {
public static void show(Animal a) {
a.eat();
// 类型判断
if (a instanceof Cat) { // 猫做的事情
Cat c = (Cat)a;
c.work();
} else if (a instanceof Dog) { // 狗做的事情
Dog c = (Dog)a;
c.work();
}
}
public static void main(String[] args) {
show(new Cat()); // 以 Cat 对象调用 show 方法
show(new Dog()); // 以 Dog 对象调用 show 方法
Animal a = new Cat(); // 向上转型: 子类对象 -> 父类对象
a.eat(); // 调用的是 Cat 的 eat
Cat c = (Cat)a; // 向下转型: 父类对象 -> 子类对象
c.work(); // 调用的是 Cat 的 work
}
}
// 运行结果
吃鱼
抓老鼠
吃骨头
看家
吃鱼
抓老鼠
8.3.4 instanceof 关键字
Java中可以使用instanceof
关键字判断对象是否是某个类的实例,语法格式如下:
对象 instanceof 类
在上述格式中,如果对象是指定类的实例对象,则返回true
,否则返回false
。
class Animal {
public String name;
public int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
public void eat() {
System.out.println(this.name + "正在吃...");
}
}
class Dog extends Animal {
public String color;
public Dog(String name, int age,String color) {
super(name,age);
this.color = color;
}
public void eat() {
System.out.println(this.name + "正在吃狗粮...");
}
public void barks() {
System.out.println(this.name + "正在旺旺叫...");
}
}
class Bird extends Animal {
public Bird(String name, int age) {
super(name, age);
}
public void eat() {
System.out.println(this.name + "正在吃鸟粮...");
}
public void fly() {
System.out.println(this.name + "正在飞...");
}
}
public class Test1 {
public static void main(String[] args) {
Animal animal1 = new Dog("旺财",10,"黄色");
if (animal1 instanceof Bird){ // 判断 dog对象是否是Bird类的实例 如果是则实例化对象,否则打印hell
Bird bird2 = (Bird)animal1;
bird2.fly();
}else {
System.out.println("hell");
}
}
}
9. 接口和抽象类
9.1 抽象类
9.1.1 抽象类的概念
在面向对象的概念中,所有的对象都是通过类来描绘的,但是放过来,并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。
9.1.2 抽象类的语法
在Java 中,一个类如果被abstract
修饰的类成为抽象类,抽象类中被abstract
修饰的方法称为抽象方法,抽象方法不用给出具体的实现体。
抽象类的定义格式如下:
abstract class 抽象类名{
属性;
// 普通方法
访问权限 返回值类型 方法名称(参数){
return [返回值];
}
// 抽象方法,无方法体
访问权限 abstract 返回值类型 抽象方法名称(参数);
}
从以上格式可以看出,抽象类的定义比普通类多了抽象方法,类的其他功能依然存在,成员变量、成员方法和构造方法的访问方式和普通类一样。
9.1.3 抽象类的特性
-
抽象类不能直接实例化对象
-
抽象方法不能是被
private
修饰的 -
抽象方法不能被
final
和static
修饰,因为抽象方法要被子类重写。 -
抽象类必须被继承,并且继承后子类要重写父类中的抽象方法,否则子类也是抽象类,必须要使用
abstract
修饰,如果一个非抽象类继承了抽象类,那么这个子类必须实现抽象类中的全部抽象方法。abstract class Shape { // 抽象方法 public abstract void draw(); } abstract class A extends Shape { public abstract void testA(); } class B extends A { @Override public void testA() { } @Override public void draw() { } }
-
抽象类中不一定包含抽象方法,但是有抽象方法的类一定是抽象类
-
抽象类中可以有构造方法,供子类创建对象时,初始化父类的成员变量
9.2 接口
接口用来描述类应该做什么,而不指定它们具体应该如何做。一个类可以实现一个或多个接口。有些情况可能要求符合这些接口,主要有这种要求,就可以使用实现了这个接口的类(即实现类)的对象。
9.2.1 接口的概念
在Java程序设计语言中,接口不是类,而是对希望符合这个接口的类的一组需求。
接口就是公共的行为规范标准,大家在实现时,只要符合规范标准,就可以通用。
在Java中,接口可以看成是:多个类的公共规范,是一种引用数据类型。
9.2.2 接口的语法规则
接口的定义格式与定义类的格式基本相同,将class
关键字换成interface
关键字,就定义了一个接口。
public interface 接口名称{
// 抽象方法
}
注意事项:
- 创建接口时,接口的命名一般以大写字母
I
开头- 接口命名一般使用“形容词”词性的单词。
9.2.3 接口的使用
接口不能直接被使用,必须有一个"实现类"来实现该接口,实现接口的所有的抽象方法。
public class 类名 interface 接口名称{ // 可以使用,分隔,实现多个接口
// ...
}
注意:子类和父类之间是
extends
继承关系,类与接口之间是implements
实现关系。
9.2.4 接口的特性
-
接口类型是一种引用类型,但是不能直接
new
接口的对象 -
接口中每一个方法都是
public
的抽象方法, 即接口中的方法会被隐式的指定为public abstract
(只能是public abstrac
t,其他修饰符都会报错) -
接口中的方法是不能在接口中实现的,只能由实现接口的类来实现
-
重写接口中方法时,不能使用默认的访问权限
-
接口中可以含有变量,但是接口中的变量会被隐式的指定为
public static final
变量
-
接口中不能有静态代码块和构造方法(编译错误)
-
接口虽然不是类,但是接口编译完成后字节码文件的后缀格式也是
.class
-
如果类没有实现接口中的所有的抽象方法,则类必须设置为抽象类
9.2.5 实现多个接口
在Java中,类和类之间是单继承的,一个类只能有一个父类,即Java中不支持多继承,但是一个类可以实现多个接口。下面通过类来表示一组动物。
abstract public class Animal {
public String name;
public Animal(String name) {
this.name = name;
}
}
另外我们再提供一组接口, 分别表示 “会飞的”, “会跑的”, “会游泳的”。
interface IFly {
void fly();
}
interface IRun {
void run();
}
interface ISwim {
void swim();
}
接下来我们创建几个具体的动物:
猫, 是会跑的。
public class Cat extends Animal implements IRun{
public Cat(String name) {
super(name);
}
@Override
public void run() {
System.out.println(this.name + "正在用四条腿跑");
}
}
鱼,是会游泳的
public class Fish extends Animal implements ISwim{
public Fish(String name) {
super(name);
}
@Override
public void swim() {
System.out.println(this.name + "正在游泳");
}
}
青蛙,既能跑,又能游泳(两栖动物)
public class Fish extends Animal implements IRun,ISwim{
public Fish(String name) {
super(name);
}
@Override
public void run() {
System.out.println(this.name + "正在用两条腿跑");
}
@Override
public void swim() {
System.out.println(this.name + "正在游泳");
}
}
注意:一个类实现多个接口,每个接口的抽象方法都要实现,否则类必须设置为抽象类
而鸭子即可以飞,又能跑、还可以游泳。
public class Duck extends Animal implements IFly,IRun,ISwim{
public Duck(String name) {
super(name);
}
@Override
public void fly() {
System.out.println(this.name + "正在用两个翅膀飞");
}
@Override
public void run() {
System.out.println(this.name + "正在用两条腿跑");
}
@Override
public void swim() {
System.out.println(this.name + "正在用两条腿游泳");
}
}
上面的代码展示了 Java 面向对象编程中最常见的用法: 一个类继承一个父类, 同时实现多种接口。
继承表达的含义是 is-a
语义, 而接口表达的含义是 具有 xxx
特性 .
猫是一种动物,具有跑的特性。
鱼是一种动物,具有游泳的特性。
青蛙是一种,具有跑和游泳的特性。
鸭子是一种动物,具有跑、游泳和飞的特性。
这样设计有什么好处呢? 时刻牢记多态的好处, 让程序猿忘记类型. 有了接口之后, 类的使用者就不必关注具体类型,而只关注某个类是否具备某种能力.
例如:现在定义一个方法:testRun
public static void testRun(IRun iRun) {
iRun.run();
}
在这个testRun
方法内部, 我们并不关注到底是哪种动物, 只要参数是会跑的, 就行。
public static void main(String[] args) {
testRun(new Bird("布谷"));
testRun(new Duck("唐老鸭"));
testRun(new Dog("旺财"));
}
// 运行结果
布谷正在用两条腿跑
唐老鸭正在用两条腿跑
旺财正在用四条腿跑
甚至参数可以不是 “动物”, 只要会跑!
class Roboot implements IRun{
@Override
public void run() {
System.out.println("机器人正在用两条腿跑");
}
}
public class Test {
public static void testRun(IRun iRun) {
iRun.run();
}
public static void main(String[] args) {
testRun(new Roboot());
}
}
// 运行结果
机器人正在用两条腿跑
9.2.6 接口之间的继承
在Java中,类和类之间是单继承的,一个类可以实现多个接口,接口与接口之间可以多继承。即:用接口可以达到多继承的目的。
接口可以继承一个接口, 达到复用的效果. 使用 extends
关键字.
interface IRun {
void run();
}
interface ISwim {
void swim();
}
// 两栖的动物, 既能跑, 也能游
interface IAmphibious extends IRun,ISwim{
}
public class Frog extends Animal implements IAmphibious{
...
}
通过接口继承创建一个新的接口IAmphibious
表示 “两栖的”. 此时实现接口创建的 Frog
类, 就继续要实现 run
方法, 也需要实现 swim
方法.
接口间的继承相当于把多个接口合并在一起.
9.3 抽象类和接口的区别
抽象类和接口都是 Java 中多态的常见使用方式. 都需要重点掌握. 同时又要认清两者的区别。
核心区别: 抽象类中可以包含普通方法和普通字段, 这样的普通方法和字段可以被子类直接使用(不必重写), 而接口中不能包含普通方法, 子类必须重写所有的抽象方法。
如之前写的 Animal
例子. 此处的 Animal
中包含一个 name
这样的属性, 这个属性在任何子类中都是存在的. 因此此处的 Animal
只能作为一个抽象类, 而不应该成为一个接口.
public class Animal {
public String name;
public Animal(String name) {
this.name = name;
}
}
区别 | 抽象类 | 接口 |
---|---|---|
结构组成: | 普通类 + 抽象方法 | 抽象方法 + 静态常量 |
权限: | 各种权限 | public |
子类使用: | 使用extends 关键字继承抽象类 |
使用implements 关键字实现接口 |
关系: | 一个抽象类可以实现若干接口 | 接口不能继承抽象类,但是接口可以使用extends 关键字继承多个父接口 |
子类权限: | 一个子类只能继承一个抽象类 | 一个子类可以实现多个接口 |
10. String字符串
在程序开发中经常会用到字符串。字符串是指一连串的字符,它是由许多单个字符连接而成的,如多个英文字母所组成的英文单词。字符串可以包含任意字符,这些字符必须包含在一对双引号""之内,例如:“abc”。Java定义了3个封装字符串的类,分别是String
类、StringBuffer
类和StringBulider
类。它们位于java.lang
包中,并提供了一系列操作字符串的方法,这些方法不需要导包就可以直接使用。下面将对String
类、StringBuffer
类和StringBulider
类进行讲解。
10.1 String类
10.1.1 字符串的构造
String
类提供了构造方法非常多,常用的就以下三种:
public class Main {
public static void main(String[] args) {
// 使用常量串构造
String s1 = "hello";
System.out.println(s1);
// 直接new String对象
String s2 = new String("hello");
System.out.println(s2);
// 使用字符数组进行构造
char[] chars = {'h','e','l','l','o'};
String s3 = new String(chars);
System.out.println(s3);
}
}
其他方法需要用到时,大家参考Java在线文档:String官方文档
【注意】:
-
String是引用类型,内部并不存储字符串本身,在String类的实现源码中,String类实例变量如下:
public class Main { public static void main(String[] args) { // s1和s2引用的是不同对象 s1和s3引用的是同一对象 String s1 = new String("hello"); String s2 = new String("world"); String s3 = s1; // s3这个引用指向了s1这个引用的对象 System.out.println(s3); // hello System.out.println(s1.length());// 获取字符串的长度 System.out.println(s1.isEmpty());// 如果字符串长度为0,返回true,否则返回false String s4 = ""; System.out.println(s4.length()); // 0 System.out.println(s4.isEmpty());// true }
内存图:
-
在Java中""引起来的也是String类型对象
// 打印"hello"字符串(String对象)的长度 System.out.println("hello".length());// 5
10.1.2 String对象的比较
字符串的比较是常见操作之一,比如:字符串排序。Java中总共提供了4种方式:
1. ==比较是否引用同一个对象
注意:对于内置类型,==
比较的是变量中的值;对于引用类型==
比较的是引用中的地址。
public class Main {
public static void main(String[] args) {
int a = 10;
int b = 20;
int c = 10;
// 对于基本类型变量,==比较两个变量中存储的值是否相同
System.out.println(a == b); // false
System.out.println(a == c); // true
// 对于引用类型变量,==比较两个引用变量引用的是否为同一个对象
String s1 = new String("hello");
String s2 = new String("hello");
String s3 = new String("world");
String s4 = s1;
System.out.println(s1 == s2); // false
System.out.println(s2 == s3); // false
System.out.println(s1 == s4); // true
}
}
2. equals()方法:按照字典序比较
字典序:字符大小的顺序
String
类重写了父类Object
中equals
方法,Object
中equals
默认按照==
比较,String
重写equals
方法后,按照 如下规则进行比较,比如:s1.equals(s2)
public class Main {
public static void main(String[] args) {
String s1 = new String("hello");
String s2 = new String("hello");
String s3 = new String("Hello");
// s1、s2、s3引用的是三个不同对象,因此==比较结果全部为false
System.out.println(s1 == s2); // false
System.out.println(s1 == s3); // false
// equals比较:String对象中的逐个字符
// 虽然s1与s2引用的不是同一个对象,但是两个对象中放置的内容相同,因此输出true
// s1与s3引用的不是同一个对象,而且两个对象中内容也不同,因此输出false
System.out.println(s1.equals(s2)); // true
System.out.println(s1.equals(s3)); // false
}
}
【注意】:为什么以下代码输出的结果都是true
?
答:因为在 Java 中有一块特殊的内存(常量池),存储在堆上。
它的作用是什么呢?
- 只要是""双引号括起来的字符串存放在这里。
- 存储字符串之前它会找常量池里是否存在这个字符串,如果有就不存放了(常量池不会重复存放相同的值),所以上述代码中
s1
和s2
都指向常量池hello
的地址。
3. ***pareTo()方法: 按照字典序进行比较
与equals
不同的是,equals
返回的是boolean
类型,而***pareTo
返回的是int类型。具体比较方式:
- 先按照字典次序大小比较,如果出现不等的字符,直接返回这两个字符的大小差值
- 如果前k个字符相等(k为两个字符长度最小值),返回值两个字符串长度差值
public class Main {
public static void main(String[] args) {
String s1 = new String("abc");
String s2 = new String("ac");
String s3 = new String("abc");
String s4 = new String("abcdef");
// s1 和 s2 比较大小 s1 > s2 返回大于0的数字 s1 < s2 返回小于0的数字 否则返回0
// 返回差值就是对应acsii码的差值
System.out.println(s1.***pareTo(s2)); // 不同输出字符差值-1
System.out.println(s1.***pareTo(s3)); // 相同输出 0
System.out.println(s1.***pareTo(s4)); // 前k个字符完全相同,输出长度差值 -3
}
}
4. 忽略大小写比较
-
equalsIgnoreCase()
方法:与equals()
方式相同,但是忽略大小写比较。 -
***pareToIgnoreCase()
方法:与***pareTo()
方式相同,但是忽略大小写比较。
public class Main {
public static void main(String[] args) {
String s1 = new String("abc");
String s2 = new String("Abc");
System.out.println(s1.equals(s2)); // false
System.out.println(s1.equalsIgnoreCase(s2)); // true
System.out.println(s1.***pareTo(s2));//32
System.out.println(s1.***pareToIgnoreCase(s2));// 0
}
}
10.1.3 字符串查找
字符串查找也是字符串中非常常见的操作,String类提供的常用查找的方法
方法 | 功能 |
---|---|
char charAt(int index) | 返回index位置上字符,如果index为负数或者越界,抛出 IndexOutOfBoundsException异常 |
int indexOf(int ch) | 返回ch第一次出现的位置,没有返回-1 |
int indexOf(int ch, int fromIndex) | 从fromIndex位置开始找ch第一次出现的位置,没有返回-1 |
int indexOf(String str) | 返回str第一次出现的位置,没有返回-1 |
int indexOf(String str, int fromIndex) | 从fromIndex位置开始找str第一次出现的位置,没有返回-1 |
int lastIndexOf(int ch) | 从后往前找,返回ch第一次出现的位置,没有返回-1 |
int lastIndexOf(int ch, int fromIndex) | 从fromIndex位置开始找,从后往前找ch第一次出现的位置,没有返 回-1 |
int lastIndexOf(String str) | 从后往前找,返回str第一次出现的位置,没有返回-1 |
int lastIndexOf(String str, int fromIndex) | 从fromIndex位置开始找,从后往前找str第一次出现的位置,没有返 回-1 |
public class Main {
public static void main(String[] args) {
String s1 = new String("hello");
// 返回字符串对应下标的字符
System.out.println(s1.charAt(1)); // e
//返回对应字符出来的下标位置 从头开始查找
System.out.println(s1.indexOf('e')); // 1
//返回对应字符出来的下标位置 从指定位置查找
System.out.println(s1.indexOf('l', 3)); // 3
// 字符串查找 从一个字符串找另一个字符串
System.out.println(s1.indexOf("llo")); // 2
System.out.println(s1.indexOf("ll", 2));// 2
// 返回对应字符出来的下标位置 从尾开始向前查找
System.out.println(s1.lastIndexOf('l'));// 3
// 返回对应字符出来的下标位置 从指定位置向前查找
System.out.println(s1.lastIndexOf('l', 1));// -1
System.out.println(s1.lastIndexOf("ll")); // 2
System.out.println(s1.indexOf("ll", 1));// 2
}
}
10.1.4 转换
1. 数值和字符串转化
public class Main {
public static void main(String[] args) {
// 数字转字符串
String s1 = String.valueOf(123);
System.out.println(s1);
String s2 = String.valueOf(12.34);
System.out.println(s2);
String s3 = String.valueOf(true);
System.out.println(s3);
// 字符串转数字
int num1 = Integer.parseInt("1234");
System.out.println(num1);
double num2 = Double.parseDouble("12.34");
System.out.println(num2);
}
}
2. 大小写转化
public class Main {
public static void main(String[] args) {
// 小写转大写
String s1 = "hello";
System.out.println(s1.toUpperCase());
// 大写转小写
String s2 = "HELLO";
System.out.println(s2.toLowerCase());
}
}
问题:转化为大写/小写是在原来的字符串上进行修改的?
答:不是!!!,转化为大写/小写之后,是产生了一个新的对象
通过String
类源码中的toUpperCase()
方法和toLowerCase()
方法返回的都是一个新的字符串。
验证:
3. 字符串转数组
public class Main {
public static void main(String[] args) {
// 字符串转数组
String s1 = "hello";
char[] chars = s1.toCharArray();
for (char ch : chars) {
System.out.println(ch);
}
// 数组为字符串
String s2 = new String(chars);
System.out.println(s2);
}
}
4. 格式化
public class Main {
public static void main(String[] args) {
String s1 = String.format("%d-%d-%d",2021,5,19);
System.out.println(s1);
}
}
10.1.5 字符串替换
使用一个指定的新的字符串替换掉已有的字符串数据,可用的方法如下:
方法 | 说明 |
---|---|
String replaceAll(String regex, String replacement) | 替换所有的指定内容 |
String replaceFirst(String regex, String replacement) | 替换收个内容 |
public class Main {
public static void main(String[] args) {
String s1 = "abcabcdeabcd";
System.out.println(s1.replace('a', 'p')); // pbcpbcdepbcd
System.out.println(s1.replace("ab","haha")); // hahachahacdehahacd
System.out.println(s1.replaceAll("ab", "uuu")); // uuucuuucdeuuucd
System.out.println(s1.replaceFirst("ab", "ha")); // hacabcdeabcd
}
}
10.1.6 字符串拆分
可以将一个完整的字符串按照指定的分隔符划分为若干个子字符串。
方法 | 功能 |
---|---|
String[] split(String regex) | 将字符串全部拆分 |
String[] split(String regex, int limit) | 将字符串以指定的格式,拆分为limit组 |
public class Main {
public static void main(String[] args) {
String s1 = "name = zhangsan&age = 18";
String[] strings = s1.split("&");
for (int i = 0; i < strings.length; i++) {
System.out.println(strings[i]);
}
String s2 = "Hello handsome hello beautiful give me some attention";
// 帅哥美女点点关注
String[] strings1 = s2.split(" ",12);
// 虽然不能分割12次 但是它能够保证能分割的最大次数 不够就不分了
for (int i = 0; i < strings1.length; i++) {
System.out.println(strings1[i]);
}
}
}
特殊情况:
public class Main {
public static void main(String[] args) {
String s1 = "192.168.1.2";
String[] strings = s1.split("\\.");
for (int i = 0; i < strings.length; i++) {
System.out.println(strings[i]);
}
System.out.println("=========");
String s2 = "C:\\APP\\Java\\jdk1.8\\bin\\java.exe";
String[] strings1 = s2.split("\\\\");
for (int i = 0; i < strings1.length; i++) {
System.out.println(strings1[i]);
}
System.out.println("=========");
String s3 = "name=zhangsan&age=18";
String[] strings2 = s3.split("&|=");
for (int i = 0; i < strings2.length; i++) {
System.out.println(strings2[i]);
}
}
}
【注意事项】:
- 字符"|“,”*“,”+“都得加上转义字符,前面加上”\".
- 而如果是"“,那么就得写成”\\".
- 如果一个字符串中有多个分隔符,可以用"|"作为连字符.
多次拆分:
public class Main {
public static void main(String[] args) {
String s1 = "name=zhangsan&age=18";
String[] strings = s1.split("&");
for (String x:strings) {
String[] strings2 = x.split("=");
for (String x1 :strings2) {
System.out.println(x1);
}
}
}
}
10.1.7 字符串的截取
从一个完整的字符串之中截取出部分内容。可用方法如下:
方法 | 功能 |
---|---|
String substring(int beginIndex) | 从指定索引截取到结尾 |
String substring(int beginIndex, int endIndex) | 截取部分内容 |
public class Main {
public static void main(String[] args) {
String s1 = "helloworld" ;
System.out.println(s1.substring(5)); // world
System.out.println(s1.substring(0, 5));
// hello 包含 0 下标的字符, 不包含 5 下标
}
}
10.1.8 其他操作
方法 | 功能 |
---|---|
String trim() | 去掉字符串中的左右空格,保留中间空格 |
代码案例:trim()
方法:
public class Main {
public static void main(String[] args) {
String str = " hello world " ;
System.out.println("["+str+"]");// [ hello world ]
System.out.println("["+str.trim()+"]");// [hello world]
}
}
trim
会去掉字符串开头和结尾的空白字符(空格, 换行, 制表符等).
10.2. StringBuilde 类 和 StringBuffer类
由于String的不可更改特性,为了方便字符串的修改,Java中又提供StringBuilder
和StringBuffer
类。这两个类大 部分功能是相同的,这里介绍 StringBuilder
常用的一些方法,其它需要用到了大家可参阅 [StringBuilder在线文档](Overview (Java Platform SE 8 ) (oracle.***))
方法 | 功能 |
---|---|
StringBuff append(String str) | 在尾部追加,相当于String的+=,可以追加:boolean、char、char[]、 double、float、int、long、Object、String、StringBuff的变量 |
char charAt(int index) | 获取index位置的字符 |
int length() | 获取字符串的长度 |
int capacity() | 获取底层保存字符串空间总的大小 |
void ensureCapacity(int mininmumCapacity) | 扩容 |
void setCharAt(int index, char ch) | 将index位置的字符设置为ch |
int indexOf(String str) | 返回str第一次出现的位置 |
int indexOf(String str, int fromIndex) | 从fromIndex位置开始查找str第一次出现的位置 |
int lastIndexOf(String str) | 返回最后一次出现str的位置 |
int lastIndexOf(String str, int fromIndex) | 从fromIndex位置开始找str最后一次出现的位置 |
StringBuff insert(int offset, String str) | 在offset位置插入:八种基类类型 & String类型 & Object类型数据 |
StringBuffer deleteCharAt(int index) | 删除index位置字符 |
StringBuffer delete(int start, int end) | 删除[start, end)区间内的字符 |
StringBuffer replace(int start, int end, String str) | 将[start, end)位置的字符替换为str |
String substring(int start) | 从start开始一直到末尾的字符以String的方式返回 |
String substring(int start,int end) | 将[start, end)范围内的字符以String的方式返回 |
StringBuffer reverse() | 反转字符串 |
String toString() | 将所有字符按照String的方式返回 |
public class Main {
public static void main(String[] args) {
StringBuilder sb1 = new StringBuilder("hello");
StringBuilder sb2 = sb1;
// 追加:即尾插-->字符、字符串、整形数
sb1.append(' '); // hello
sb1.append("world"); // hello world
sb1.append(123); // hello world123
System.out.println(sb1); // hello world123
System.out.println(sb1 == sb2); // true
System.out.println(sb1.charAt(0)); // 获取0号位上的字符 h
System.out.println(sb1.length()); // 获取字符串的有效长度14
System.out.println(sb1.capacity()); // 获取底层数组的总大小
sb1.setCharAt(0, 'H'); // 设置任意位置的字符 Hello world123
sb1.insert(0, "Hello world!!!"); // Hello world!!!Hello world123
System.out.println(sb1);
System.out.println(sb1.indexOf("Hello")); // 获取Hello第一次出现的位置
System.out.println(sb1.lastIndexOf("hello")); // 获取hello最后一次出现的位置
sb1.deleteCharAt(0); // 删除首字符
sb1.delete(0,5); // 删除[0, 5)范围内的字符
String str = sb1.substring(0, 5); // 截取[0, 5)区间中的字符以String的方式返回
System.out.println(str);
sb1.reverse(); // 字符串逆转
str = sb1.toString(); // 将StringBuffer以String的方式返回
System.out.println(str);
}
}
从上述例子可以看出:String
和StringBuilder
最大的区别在于**String
的内容无法修改**,而StringBuilder
的内容可 以修改。频繁修改字符串的情况考虑使用StringBuilder
。
注意:String
和StringBuilder
类不能直接转换。如果要想互相转换,可以采用如下原则:
-
String
变为StringBuilder
: 利用StringBuilder
的构造方法或append()
方法 -
StringBuilder
变为String
: 调用toString()
方法。
11. 异常
11.1 异常的概念和体系结构
11.1.1 异常的概念
在生活中,一个人表情痛苦,出于关心,可能会问:你是不是生病了,需要我陪你去看医生吗?
在程序中也是一样,程序猿是一帮办事严谨、追求完美的人才。在日常开发中,绞尽脑汁将代码写的完美,在程序运行过程中,难免会出现一些奇奇怪怪的问题,有时通过代码很难去控制,比如:数据格式不对、网络不通畅、内存报警等。
在 Java中,将程序执行过程中发生的不正常行为称为异常。比如之前写代码时经常遇到的:
-
算术异常
-
数组越界异常
-
空指针异常
从上述过程中可以看到,Java 中不同类型的的异常,都有与其对应的类进行描述
11.1.2 异常体系
异常种类繁多,为了对不同异常或者错误进行很好的分类管理,Java内部维护了一个异常的体系结构:
从上图中可以看到:
-
Throwable
:是异常体系的顶层类,其派生出两个重要的子类,Error
和Exception
-
Error
:**指的是Java虚拟机无法解决的严重问题,**比如:JVM 的内部错误、资源耗尽等,典型代表:StackOverflowError
和OutOfMemoryError
,一旦发生回力乏术。 -
Exception
:异常产生后程序员可以通过代码进行处理,使程序继续执行。比如:感冒、发烧。我们平时所说的异常就是Exception
。
11.1.3 异常的分类
异常可能在编译时发生,也可能在程序运行时发生,根据发生的时机不同,可以将异常分为:
-
编译时异常
在程序编译期间发生的异常,称为编译时异常,也称为受检查异常(Checked Exception)
-
运行时异常
在程序执行期间发生的异常,称为运行时异常,也称为非受检查异常(Unchecked Exception)
RunTimeException
:以及其子类对应的异常,都称为运行时异常。比如:NullPointerException
、ArrayIndexOutOfBoundsException
、ArithmeticException
。
【注意】:编译时出现的语法性错误,不能称之为异常。例如将System.out.println
拼写错了, 写成了System.out.println
. 此时编译过程中就会出错, 这是 “编译期” 出错。而运行时指的是程序已经编译通过得到class
文件了, 再由 JVM 执行过程中出现的错误.
11.2 异常的处理
11.2.1 防御式编程
错误在代码中是客观存在的. 因此我们要让程序出现问题的时候及时通知程序猿. 主要的方式
-
LBYL: Look Before You Leap. 在操作之前就做充分的检查. 即:事前防御型
boolean ret = false; ret = 登陆游戏(); if (!ret) { 处理登陆游戏错误; return; } ret = 开始匹配(); if (!ret) { 处理匹配错误; return; } ret = 游戏确认(); if (!ret) { 处理游戏确认错误; return; } ret = 选择英雄(); if (!ret) { 处理选择英雄错误; return; } ret = 载入游戏画面(); if (!ret) { 处理载入游戏错误; return; } ......
缺陷:正常流程和错误处理流程代码混在一起, 代码整体显的比较混乱。
-
EAFP: It’s Easier to Ask Forgiveness than Permission. “事后获取原谅比事前获取许可更容易”. 也就是先操作, 遇到问题再处理. 即:事后认错型
try { 登陆游戏(); 开始匹配(); 游戏确认(); 选择英雄(); 载入游戏画面(); ... } catch (登陆游戏异常) { 处理登陆游戏异常; } catch (开始匹配异常) { 处理开始匹配异常; } catch (游戏确认异常) { 处理游戏确认异常; } catch (选择英雄异常) { 处理选择英雄异常; } catch (载入游戏画面异常) { 处理载入游戏画面异常; } ......
优势:正常流程和错误流程是分离开的, 程序员更关注正常流程,代码更清晰,容易理解代码
异常处理的核心思想就是 EAFP。
在Java中,异常处理主要的5个关键字:throw、try、catch、finally、throws
11.2.2 异常的抛出
在编写程序时,如果程序中出现错误,此时就需要将错误的信息告知给调用者,比如:参数检测。
在Java中,可以借助throw
关键字,抛出一个指定的异常对象,将错误信息告知给调用者。具体语法如下:
throw new XXXException("异常产生的原因");
代码示例:
public class Test {
private static int getSubscript(int[] arr, int index) {
if (arr == null) {
throw new NullPointerException("数组为空");
}
if (index < 0 || index >= arr.length) {
throw new ArithmeticException("下标越界");
}
return arr[index];
}
public static void main(String[] args) {
int[] arr = {1,2,3};
getSubscript(arr,3);
}
}
运行结果:
一般情况下通过throw
抛出的是自定义的异常
【注意事项】
-
throw
必须写在方法体内部 - 抛出的对象必须是
Exception
或者Exception
的子类对象 - 如果抛出的是
RunTimeException
或者RunTimeException
的子类,则可以不用处理,直接交给JVM来处理 - 如果抛出的是编译时异常,用户必须处理,否则无法通过编译
- 异常一旦抛出,其后的代码就不会执行
11.2.3 异常的捕获
异常的捕获,也就是异常的具体处理方式,主要有两种:异常声明throws
以及 try-catch
捕获处理。
11.2.3.1 异常声明throws
处在方法声明时参数列表之后,当方法中抛出编译时异常,用户不想处理该异常,此时就可以借助throws
将异常抛给方法的调用者来处理。即当前方法不处理异常,提醒方法的调用者处理异常。
语法格式:
修饰符 返回值类型 方法名(参数列表) throws 异常类型1,异常类型2...{
}
【注意事项】
-
throws
必须跟在方法的参数列表之后 -
声明的异常必须是
Exception
或者Exception
的子类 -
方法内部如果抛出了多个异常,
throws
之后必须跟多个异常类型,之间用逗号隔开,如果抛出多个异常类型具有父子关系,直接声明父类即可。
- 调用声明抛出异常的方法时,调用者必须对该异常进行处理,或者继续使用
throws
抛出
11.2.3.2 try - catch捕获并处理
throws
对异常并没有真正处理,而是将异常报告给抛出异常方法的调用者,由调用者处理。如果真正要对异常进行处理,就需要try-catch
。
语法格式:
try{
// 将可能出现异常的代码放在这里
}catch(要捕获的异常类型 e){
// 如果try中的代码抛出异常了,此处catch捕获时异常类型与try中抛出的异常类型一致时,或者是try中抛出异常的基类
时,就会被捕获到
// 对异常就可以正常处理,处理完成后,跳出try-catch结构,继续执行后序代码
}[catch(异常类型 e){
// 对异常进行处理
}finally{
// 此处代码一定会被执行到
}]
// 后序代码
// 当异常被捕获到时,异常就被处理了,这里的后序代码一定会执行
// 如果捕获了,由于捕获时类型不对,那就没有捕获到,这里的代码就不会被执行
注意:
- []中表示可选项,可以添加,也可以不用添加
- try中的代码可能会抛出异常,也可能不会
代码示例
public class Test {
public static void fun() throws NullPointerException {
int[] arr = null;
System.out.println(arr.length);
}
public static void main(String[] args){
try {
fun();
} catch (NullPointerException e) {
e.printStackTrace();
System.out.println("处理NullPointerException异常!");
}
System.out.println("haha");
}
}
运行结果:
关于异常的处理方式
异常的种类有很多, 我们要根据不同的业务场景来决定.
对于比较严重的问题(例如和算钱相关的场景), 应该让程序直接崩溃, 防止造成更严重的后果
对于不太严重的问题(大多数场景), 可以记录错误日志, 并通过监控报警程序及时通知程序猿
对于可能会恢复的问题(和网络相关的场景), 可以尝试进行重试.
在我们当前的代码中采取的是经过简化的第二种方式. 我们记录的错误日志是出现异常的方法调用信息, 能很快速的让我们找到出现异常的位置. 以后在实际工作中我们会采取更完备的方式来记录异常信息.
【注意事项】:
-
try
块内抛出异常位置之后的代码将不会被执行 -
如果抛出异常类型与
catch
时异常类型不匹配,即异常不会被成功捕获,也就不会被处理,继续往外抛,直到 JVM 收到后中断程序----异常是按照类型来捕获的
-
try
中可能会抛出多个不同的异常对象,则必须用多个catch
来捕获----即多种异常,多次捕获
public class Test {
public static void main(String[] args) {
try {
int[] arr = {1,2,3};
System.out.println(arr[9]);
} catch (NullPointerException e) {
e.printStackTrace();
System.out.println("捕获NullPointerException异常!");
} catch (ArrayIndexOutOfBoundsException e) {
e.printStackTrace();
System.out.println("捕获ArrayIndexOutOfBoundsException异常");
}
System.out.println("后续代码");
}
}
如果多个异常的处理方式是完全相同, 也可以写成这样:
public class Test {
public static void main(String[] args) {
try {
int[] arr = {1,2,3};
System.out.println(arr[9]);
} catch (NullPointerException | ArrayIndexOutOfBoundsException e) {
e.printStackTrace();
System.out.println("捕获NullPointerException或者ArrayIndexOutOfBoundsException异常!");
}
System.out.println("后续代码");
}
}
如果异常之间具有父子关系,一定是子类异常在前catch,父类异常在后catch,否则语法错误:
正确形式:
public class Test {
public static void main(String[] args) {
try {
int[] arr = {1,2,3};
System.out.println(arr[9]);
} catch (NullPointerException | ArrayIndexOutOfBoundsException e) {
e.printStackTrace();
System.out.println("捕获NullPointerException或者ArrayIndexOutOfBoundsException异常!");
} catch (RuntimeException e) {
e.printStackTrace();
}
System.out.println("后续代码");
}
}
- 可以通过一个
catch
捕获所有的异常,即多个异常,一次捕获(不推荐)
public class Test {
public static void main(String[] args) {
try {
int[] arr = {1,2,3};
System.out.println(arr[9]);
} catch (Exception e) {
e.printStackTrace();
System.out.println("捕获异常!");
}
System.out.println("后续代码");
}
}
由于 Exception
类是所有异常类的父类. 因此可以用这个类型表示捕捉所有异常
备注:
catch
进行类型匹配的时候, 不光会匹配相同类型的异常对象, 也会捕捉目标异常类型的子类对象.如刚才的代码,NullPointerException
和ArrayIndexOutOfBoundsException
都是Exception
的子类,因此都能被捕获到.
11.2.3.3 finally
在写程序时,有些特定的代码,不论程序是否发生异常,都需要执行,比如程序中打开的资源:网络连接、数据库连接、IO流等,在程序正常或者异常退出时,必须要对资源进进行回收。另外,因为异常会引发程序的跳转,可能导致有些语句执行不到,finally
就是用来解决这个问题的。
语法格式:
try{
// 可能会发生异常的代码
}catch(异常类型 e){
// 对捕获到的异常进行处理
}finally{
// 此处的语句无论是否发生异常,都会被执行到
}
// 如果没有抛出异常,或者异常被捕获处理了,这里的代码也会执行
代码示例
public class Test {
public static void main(String[] args) {
try {
int[] arr = {1,2,3};
System.out.println(arr[9]);
} catch (ArrayIndexOutOfBoundsException e) {
e.printStackTrace();
System.out.println("捕获ArrayIndexOutOfBoundsException异常");
} finally {
System.out.println("finally被执行了");
}
System.out.println("后续代码");
}
}
问题:既然 finally 和 try-catch-finally 后的代码都会执行,那为什么还要有finally呢?
代码示例:
public class Test {
public static int func() {
Scanner input = new Scanner(System.in);
try {
int a = input.nextInt();
return a; // 这个return 不影响finally执行
} catch (In***patibleClassChangeError e) {
e.printStackTrace();
System.out.println("捕获到In***patibleClassChangeError异常");
} finally {
System.out.println("finally被执行了");
}
if (input != null) {
input.close();
}
return -1;
}
public static void main(String[] args) {
System.out.println(func());
}
}
上述程序,如果正常输入,成功接收输入后程序就返回了,try-catch-finally
之后的代码根本就没有执行,即输入流就没有被释放,造成资源泄漏。
优化代码:
public class Test {
public static int func() {
Scanner input = new Scanner(System.in);
try {
int a = input.nextInt();
return a; // 这个return 不影响finally执行
} catch (In***patibleClassChangeError e) {
e.printStackTrace();
System.out.println("捕获到In***patibleClassChangeError异常");
} finally {
System.out.println("finally被执行了");
input.close();
}
return -1;
}
public static void main(String[] args) {
System.out.println(func());
}
}
注意:finally
中的代码一定会执行的,一般在finally
中进行一些资源清理的扫尾工作
11.2.4 异常的处理流程
关于 “调用栈”
方法之间是存在相互调用关系的, 这种调用关系我们可以用 “调用栈” 来描述. 在 JVM 中有一块内存空间称为"虚拟机栈" 专门存储方法之间的调用关系. 当代码中出现异常的时候, 我们就可以使用e.printStackTrace();
的方式查看出现异常代码的调用栈.
如果本方法中没有合适的处理异常的方式, 就会沿着调用栈向上传递
如果向上一直传递都没有合适的方法处理异常, 最终就会交给 JVM 处理, 程序就会异常终止(和我们最开始未使用 try-catch
时是一样的).
可以看到, 程序已经异常终止了, 没有执行到 System.out.println("后续代码");
这一行
优化代码
public class Test {
public static int func() {
try {
int[] arr = {1,2,3};
System.out.println(arr[10]);
} catch (NullPointerException e) {
e.printStackTrace();
System.out.println("捕获到NullPointerException异常");
} finally {
System.out.println("finally被执行了");
}
System.out.println("后续代码");
return -1;
}
public static void main(String[] args) {
try {
System.out.println(func());
} catch (ArrayIndexOutOfBoundsException e) { // main方法处理异常
e.printStackTrace();
System.out.println("ArrayIndexOutOfBoundsException");
} finally {
}
System.out.println("后续代码...");
}
}
【异常处理流程总结】
- 程序先执行
try
中的代码 - 如果
try
中的代码出现异常, 就会结束try
中的代码, 看和catch
中的异常类型是否匹配. - 如果找到匹配的异常类型, 就会执行
catch
中的代码 - 如果没有找到匹配的异常类型, 就会将异常向上传递到上层调用者.
- 无论是否找到匹配的异常类型,
finally
中的代码都会被执行到(在该方法结束之前执行). - 如果上层调用者也没有处理的了异常, 就继续向上传递.
- 一直到
main
方法也没有合适的代码处理异常, 就会交给 JVM 来进行处理, 此时程序就会异常终止
11.3 自定义异常
Java 中虽然已经内置了丰富的异常类, 但是并不能完全表示实际开发中所遇到的一些异常,此时就需要维护符合我们实际情况的异常结构.
例如, 我们实现一个用户登陆功能
class Login {
public String userName = "admin";
public String passWord = "12345";
public void login(String userName, String passWord) {
if (!this.userName.equals(userName)) {
System.out.println("用户名错误");
}
if (!this.passWord.equals(passWord)) {
System.out.println("密码错误");
}
}
}
public class Test {
public static void main(String[] args) {
Login login = new Login();
login.login("admin","123");
}
}
此时我们在处理用户名密码错误的时候可能就需要抛出两种异常. 我们可以基于已有的异常类进行扩展(继承), 创建和我们业务相关的异常类
具体方式:
- 自定义异常类,然后继承自
Exception
或者RunTimeException
- 实现一个带有
String
类型参数的构造方法,参数含义:出现异常的原因
public class UserNameException extends RuntimeException{
public UserNameException() {
super();
}
public UserNameException(String message) {
super(message);
}
}
public class PasswordException extends RuntimeException{
public PasswordException() {
super();
}
public PasswordException(String message) {
super(message);
}
}
此时我们的 login
代码可以改成
class Login {
public String userName = "admin";
public String passWord = "12345";
public void login(String userName, String passWord) {
if (!this.userName.equals(userName)) {
//System.out.println("用户名错误");
throw new UserNameException("用户名错误");
}
if (!this.passWord.equals(passWord)) {
//System.out.println("密码错误");
throw new PasswordException("密码错误");
}
}
}
public class Test {
public static void main(String[] args) {
Login login = new Login();
try {
login.login("admin","123");
} catch (UserNameException e) {
e.printStackTrace();
} catch (PasswordException e) {
e.printStackTrace();
}
System.out.println("后续代码...");
}
}
【注意事项】
-
自定义异常通常会继承自
Exception
或者RuntimeException
-
继承自
Exception
的异常默认是受查异常 -
继承自
RuntimeException
的异常默认是非受查异常