Java基础
第一阶段
第一阶段 目标:建立编程思想
Java就业方向
- Java EE软件工程师:电商、团购、众筹、sns、教育、金融、搜索
- 大数据软件工程师:大数据引用工程师、大数据算法工程师、大数据分析和挖掘
- Android软件工程师:android开发工程师
1、Java概述
写第一个Java小程序
-
创建txt文本,写入如下内容,并保存为
Helloworld.java
public class Helloworld { public static void main(String[] args) { System.out.println("Hello World!!!"); } }
-
进入
Helloworld.java
的保存路径,打开cmd -
在DOS窗口中输入:
javac Helloworld.java
-
继续输入
java Helloworld
-
显示结果
Hello World!!!
1.1、Java历史
- 1990,sun公司 启动绿色计划
- 1991,创建 oak(橡树)语言,后发现 oak已被注册,看见咖啡上冒热气—>改名为:Java
- 1994,gosling 参加 硅谷大会 演示 Java 功能,震惊世界
- 1995,sun 公司正式发布 Java 1.0版本
- 2009年,甲骨文公司(Oracle 公司)宣布收购 sun 公司
- 2011年,甲骨文公司(Oracle 公司)发布 Java 7
- 2014年,甲骨文公司(Oracle 公司)发布 Java 8(长期支持版)
- JDK 8和 JDK 11是社会上最主流的版本
- Oracle官网:https://www.oracle.com/
- 其他JDK版本如下:
Java技术体系平台
-
Java SE(Java Standard Edition) 标准版
支持面向桌面级应用(Windows下的应用程序)的Java平台,提供了完整的Java核心API,此版本以前称为 J2SE。
-
Java EE(Java Enterprise Edition) 企业版
开发企业环境下的应用程序提供的一套解决方案。该技术体系中包含:Servlet、JSP等,主要针对于Web应用开发,此版本之前为J2EE。
-
Java ME(Java Micro Edition) 小型版
支持Java程序运行在移动端(手机、PDA)上的平台,对Java API有所精简,并加入了针对移动端的支持,此版本之前为 J2ME。
1.2、Java特点
-
面向对象(OOP)
-
健壮性。Java的强类型机制、异常处理、垃圾的自动收集等是Java程序健壮性的重要保证。
-
跨平台性。即:一个编译好的
.class
文件可以在多个系统下运行(只需要对应系统下安装对应的虚拟机),这种特性称为跨平台性。 -
解释型语言(JS、PHP、Java),编译型语言:C/C++
解释型语言:编译后的代码,不能被机器执行,需要解释器来执行
编译型语言:编译后的代码,可以直接被机器执行
1.3、Java运行机制
Java 核心机制——Java 虚拟机(JVM,Java Virtual Machine)
1)JVM 是一个虚拟的计算机,具有指令集并使用不同的存储区域。负责执行指令、管理数据、内存、寄存器,包含在JDK中。
2)对于不同的操作系统,有不同的虚拟机。
3)Java虚拟机机制屏蔽了底层运行平台的差别,实现了“一次编译,到处运行”。
以Test.java
文件运行为例
Java运行机制(⭐⭐⭐⭐⭐)
注:.java
文件也叫源文件,.class
文件也叫字节码文件。
运行的本质就是把字节码文件加载到JVM中执行。
1.4、JDK 和 JRE
JVM基本介绍:
- JVM全称:Java Virtural Machine,Java虚拟机
- 只认识
xxx.class
文件,jvm只负责将.class
文件中的字节码指令进行识别并调用的操作系统向上的API完成动作。所以,JVM是Java能够跨平台的核心。即JVM能够跨平台 -> Java能跨平台。 - 边解释边执行
.class
字节码文件,会把字节码文件中的指令翻译成对应操作系统的机器码。
JDK基本介绍:
-
JDK全称:Java Development Kit,Java开发工具包
JDK = JRE + Java的开发工具(Java,javac,javadoc,javap等)
-
JDK是提供给Java开发人员用的,包含了Java的开发工具,也包括了JRE。
JRE基本介绍:
-
JRE全称:Java Runtime Environment ,Java运行时环境
JRE = JVM + Java的核心类库(类)
-
包括Java虚拟机(JVM)和Java程序所需的核心类库等,如果想要运行一个开发好的Java程序,计算机中只需要安装JRE即可。
JDK、JRE 和 JVM 的包含关系?(⭐⭐⭐⭐)
$JDK = JRE + 开发工具集(javac,java编译工具等)$
$JRE = JVM + Java SE 标准类库(java核心类库)$
$JDK = JVM + Java SE 标准类库 + 开发工具集$
如果只想运行编译好的.class文件,只需要JRE。
JDK的安装
-
Oracle官网下载JDK:Java Downloads | Oracle
-
选择对应版本的JDK安装程序
配置环境变量
- 右键打开【我的电脑】—>【属性】—>【高级系统设置】—>【高级】—>【环境变量】
- 在【系统变量】下,新建变量
JAVA_HOME
,路径为JDK安装路径。如:D:\Program Files (x86)\JavaJDK8
- 编辑【系统变量】中的
Path
环境变量,【新建】添加路径:%JAVA_HOME%\bin
- 打开DOS命令行(cmd),任意目录下敲入:
java
或者javac
,出现参数信息,配置成功
注:JDK安装路径不要有中文字符或特殊符号(如空格)等。
1.5、快速入门
使用DOS命令行编译源文件和运行Java程序,需要使源文件的编码格式和DOS命令行的代码页保持一致。否则会出现乱码问题。
如:源文件编码格式为GBK,则DOS命令行的代码页为936。
如图:
什么是编译?
即:javac Hello.java
- 有了Java源文件,通过编译器将其编译成JVM可以识别的字节码文件。
- 在该源文件目录下,通过javac.exe编译工具对Hello.java文件进行编译。本质就是将.class文件加载到 JVM 执行。
- 如果程序没有错误,没有任何提示,则在当前目录下会出现一个Hello.class文件,该文件称为字节码文件,也是可以执行的Java程序。
注:对于修改后的Java源文件,需要使用javac命令重新编译和运行才能使修改部分生效。
一个源文件中最多只能有一个public 类,其他类的个数不限。可以将main方法写在非 public 类中,然后指定运行非 public 类,这样入口方法就是非 public 的 main方法。
如果源文件包含一个 public 类,则文件名必须以该类名命名。
常见错误
-
找不到文件
解决方法:源文件名不存在或者写错,或者当前路径错误
-
类XXX是公共的,请在文件中说明
解决方法:声明为 public 的主类应与文件名一致,否则编译失败
-
编译失败
解决方法:注意错误出现的行数,再到源代码中指定位置改错
-
无法映射字符
解决方法:修改源文件的编码格式为GBK或者将DOS窗口的编码格式改为UTF-8
1.6、转义字符
\t
:一个制表位,实现对齐的功能
\n
:换行符
\\
:一个\
\"
:一个“
\’
:一个’
\r
:一个回车
1.7、Java开发规范
- 类、方法的注释,要以javadoc的方式来写。
- 非Java Doc的注释(单行注释和多行注释),往往是给代码的维护者看的,着重告诉读者为什么这样写,如何修改,注意什么问题等。
- 使用Tab操作,实现缩进,默认整体向右边移动,使用
shift + tab
整体向左移。 - 运算符和
=
两边加一个空格,增加代码可读性。 - 源文件使用UTF-8编码。
- 行宽度不要超过80个字符。
- 代码编写的两种风格:次行风格和行尾风格(推荐)。
1.8、Java API 文档
JDK 8在线官方文档:Java Platform Standard Edition 8 Documentation (oracle.com)
1.9、Java注释
用于注释解释程序的文字就是注释,注释提高了代码的可读性;注释是一个程序猿必须要具有的良好编程习惯,将自己的思想通过注释写出来,再用代码实现。Java中的注释类型:
-
单行注释(快捷键:Ctrl + /)
// 单行注释
-
多行注释
/** * 多行注释 * 多行注释 */
-
文档注释
/** 文档注释 * @author Yxz * @version 1.0 * @Description TODO * @date 2021-07-30 16:39 */
1.10、DOS命令
Dos介绍:Disk Operating System 磁盘操作系统
。
相对路径:从当前目录开始定位,形成的一个路径。
绝对路径:从顶级目录C盘或D盘等开始定位,形成的路径。如:D:\Program
常用Dos命令(⭐⭐⭐⭐):
- 查看当前目录的所有内容:
dir
,如:dir d:\java
- 切换到其他盘:
cd 盘符号
,如:切换到C盘cd /D c:
- 切换到当前盘的其他目录下:使用
cd 相对路径(或绝对路径)
。如:cd d:\java\JavaApplication
- 切换到上一级:
cd ..
,如:cd ..\
- 切换到根目录:
cd \
- 查看指定文件夹的所有子级目录:tree 路径。如:
tree d:\java
- 清屏操作:
cls
- 退出Dos:
exit
- 创建目录:
md
。如:在当前文件夹下创建test100文件夹
和test200文件夹
,md test100 test200
- 删除目录:rd。如:删除当前文件夹下的test100目录:
rd test100
- 拷贝文件:
copy
。如:将文件test2.txt复制到D盘Java文件目录下并且命名为temp.txt,copy test2.txt d:java\temp.txt
- 删除文件:
del
,如:删除文件test2.txt,del test2.txt
- 输入内容到文件:
echo
。将内容输入到文件里,如:echo hello > test.txt
,将hello输入到test.txt文件里。 - 输入空文件:
type
。可以将空内容输入到文件里,如:type nul > test.txt
,test.txt文件里为空。 - 剪切:
move
。将文件移动到某个文件目录下,move test.txt ..\test2\ok.txt
,将文件test.txt移动到父目录下的test2文件下并命名为ok.txt
1.11、如何学习新技术
- 需求:工作需要、跳槽(对方要求)、技术控
- 看看能否使用传统技术解决
- 引出我们学习的新技术和知识点
- 学习新技术或者知识点的基本原理和基本语法(不要考虑细节)
- 快速入门:能写出基本程序,实现CRUD(增删改查)
- 开始研究技术的注意事项、使用细节、使用规范、如何优化 ——> 学无止境,技术的魅力
2、变量
2.1、数据类型
基本数据类型
-
数值型
- 整数类型:byte[1],short[2],int[4],long[8]——用于存放整数值
- 浮点(小数)类型:float[4],double[8]——用于存放小数
-
字符型:char[2]——存放单个字符
java中char的本质是一个整数,输出时是对应unicode码对应的字符。char类型可以进行运算。
存储时:‘a’ ——> 码值97 ——> 二进制 ——> 存储
读取时:二进制 ——> 97 ——> ‘a’ ——> 显示
-
布尔型boolean[1],存放
true
或false
引用数据类型
- 类class
- 接口interface
- 数组[]
常见字符编码表
-
ASCII码(1个字节表示,一共表示128个字符,实际上一个字节可以表示256个字符,ASCII码只占用了一个字节的后面七位,最前面一位统一规定为0)
-
Unicode码(固定大小编码,使用2个字节表示字符,字母和汉字统一两个字节存储,浪费存储空间.使用Unicode没有乱码问题,可以表示65536个字符,编码0-127的字符与ASCII码一样,Unicode码兼容ASCII码)
-
UTF-8码(改进的Unicode码,大小可变的编码,字母使用1个字节,汉字使用3个字节。可以使用1-6个字节表示符号,根据不同符号变化字节长度)
-
GBK(可以表示汉字,范围广,字母使用1个字节,汉字使用2个字节)
-
GB2312(可以表示汉字,GB2312 < GBK)
-
BIG5码(存储繁体中文)
-
布尔型:boolean[1],存放
true
和false
。一般用于流程控制。
public class PrimitiveTypeTest {
public static void main(String[] args) {
// byte
System.out.println("基本类型:byte 二进制位数:" + Byte.SIZE);
System.out.println("包装类:java.lang.Byte");
System.out.println("最小值:Byte.MIN_VALUE = " + Byte.MIN_VALUE);
System.out.println("最大值:Byte.MAX_VALUE = " + Byte.MAX_VALUE);
System.out.println();
// short
System.out.println("基本类型:short 二进制位数:" + Short.SIZE);
System.out.println("包装类:java.lang.Short");
System.out.println("最小值:Short.MIN_VALUE = " + Short.MIN_VALUE);
System.out.println("最大值:Short.MAX_VALUE = " + Short.MAX_VALUE);
System.out.println();
// int
System.out.println("基本类型:int 二进制位数:" + Integer.SIZE);
System.out.println("包装类:java.lang.Integer");
System.out.println("最小值:Integer.MIN_VALUE = " + Integer.MIN_VALUE);
System.out.println("最大值:Integer.MAX_VALUE = " + Integer.MAX_VALUE);
System.out.println();
// long
System.out.println("基本类型:long 二进制位数:" + Long.SIZE);
System.out.println("包装类:java.lang.Long");
System.out.println("最小值:Long.MIN_VALUE = " + Long.MIN_VALUE);
System.out.println("最大值:Long.MAX_VALUE = " + Long.MAX_VALUE);
System.out.println();
// float
System.out.println("基本类型:float 二进制位数:" + Float.SIZE);
System.out.println("包装类:java.lang.Float");
System.out.println("最小值:Float.MIN_VALUE = " + Float.MIN_VALUE);
System.out.println("最大值:Float.MAX_VALUE = " + Float.MAX_VALUE);
System.out.println();
// double
System.out.println("基本类型:double 二进制位数:" + Double.SIZE);
System.out.println("包装类:java.lang.Double");
System.out.println("最小值:Double.MIN_VALUE = " + Double.MIN_VALUE);
System.out.println("最大值:Double.MAX_VALUE = " + Double.MAX_VALUE);
System.out.println();
// char -> 单一的 16 位 Unicode 字符
System.out.println("基本类型:char 二进制位数:" + Character.SIZE);
System.out.println("包装类:java.lang.Character");
// 以数值形式而不是字符形式将Character.MIN_VALUE输出到控制台
System.out.println("最小值:Character.MIN_VALUE = "
+ (int) Character.MIN_VALUE);
// 以数值形式而不是字符形式将Character.MAX_VALUE输出到控制台
System.out.println("最大值:Character.MAX_VALUE = "
+ (int) Character.MAX_VALUE);
}
}
/* 运行结果:
基本类型:byte 二进制位数:8
包装类:java.lang.Byte
最小值:Byte.MIN_VALUE = -128
最大值:Byte.MAX_VALUE = 127
基本类型:short 二进制位数:16
包装类:java.lang.Short
最小值:Short.MIN_VALUE = -32768
最大值:Short.MAX_VALUE = 32767
基本类型:int 二进制位数:32
包装类:java.lang.Integer
最小值:Integer.MIN_VALUE = -2147483648
最大值:Integer.MAX_VALUE = 2147483647
基本类型:long 二进制位数:64
包装类:java.lang.Long
最小值:Long.MIN_VALUE = -9223372036854775808
最大值:Long.MAX_VALUE = 9223372036854775807
基本类型:float 二进制位数:32
包装类:java.lang.Float
最小值:Float.MIN_VALUE = 1.4E-45
最大值:Float.MAX_VALUE = 3.4028235E38
基本类型:double 二进制位数:64
包装类:java.lang.Double
最小值:Double.MIN_VALUE = 4.9E-324
最大值:Double.MAX_VALUE = 1.7976931348623157E308
基本类型:char 二进制位数:16
包装类:java.lang.Character
最小值:Character.MIN_VALUE = 0
最大值:Character.MAX_VALUE = 65535
*/
注意:
- Java各整数类型有固定的范围和字段长度,不受具体OS【操作系统】的影响,以保证Java程序的可移植性。
- Java整型常量默认为
int
型,声明long
类型常量须后加l
或L
。 - Java程序中变量常声明为
int
型,除非int
型范围不够才使用long
。 bit
:计算机中的最小存储单位。byte(Byte)
:计算机中基本存储单元。- 1 Byte = 8 bit
- 浮点数在机器中存放形式:浮点数 = 符号位 + 指数位 + 尾数位。
- 尾数部分可能丢失,造成精度损失(小数都是近似值)。
- Java浮点型表示的范围和字段长度不受OS的影响(
float
4个字节,double
8个字节)。 - Java的浮点型常量默认为
double
型,声明float型常量须后加f
或F
。 - 浮点数常量有两种表示形式。如:
5.12
、10.1
、.131(=0.131)
- 通常情况下,应该使用
double
型,因为它比float
型精度更高。 - 不要对运算后的小数进行相等判断(小数运算后会出现精度丢失)
2.2、变量基本使用
变量是程序的基本组成单位。变量的三个基本要素:类型 + 名称 + 值。
public class Var01 {
public static void main(String[] args) {
int a; // 声明一个整型变量a
a = 100; // 给变量a赋值
System.out.println(a);
int b = 800; // 声明一个变量b并初始化赋值800
System.out.println(b);
}
}
注意:
- 变量表示内存中的一个存储区域(不同的变量,类型不同,占用的空间大小不同)。
- 该区域有自己的名称(变量名)和类型(数据类型)。
- 变量必须先声明,后使用。
- 该区域的数据可以在同一类型范围内不断变化。
- 变量在同一个作用域内不能重名。
- 变量 $=$ 数据类型 $+$ 变量名 $+$ 变量值。
2.3、数据类型转换
自动类型转换
当Java程序在进行赋值或者运算时,精度小(容量小)的类型自动转换为精度大(容量大)的数据类型。
数据类型按精度(容量)大小排序:
char ——> int ——> long ——> float ——> double
byte ——> short ——> int ——> long ——> float ——> double
注:
- 有多种类型的数据混合运算时,系统先自动将所有数据转成容量最大的那种数据类型,然后再进行计算。
- 将具体数值赋给变量前,须先判断是否超过变量类型的存储范围。
- 如果是变量类型赋值,会直接判断类型是否超出。
- byte、short、char之间不会相互自动转换。
- byte、short、char三者可以计算,在计算时首先转换成int型(与是否混合运算无关)。
- boolean不参与转换。
- 表达式结果的类型自动提升为操作数中最大的类型。
public class AutoConvert {
public static void main(String[] args) {
int i = 1; // 正确
int b1 = 10;
float b2 = b1 + 1.1; // 报错1:1.1默认为double型,num1+1.1为double型,不能兼容float
// 改进方法1:float b2 = b1 + 1.1F;
// 改进方法2:double b2 = b1 + 1.1;
byte c1 = 128; // 错误2:超出byte表示范围[-128, 127]
int d1 = 1;
byte d2 = d1; // 报错3:变量类型赋值,会直接判断类型是否超出接受类型的大小。不兼容的类型
byte e1 = 10;
char e2 = e1; // 报错4:byte、short、char之间不会相互自动转换,byte不能自动转换成char
byte b = 1;
short s1 = 1;
short s2 = b + s1; // 报错5:b + s1 => 结果为int型,short不能兼容int型
// 改进方法:short s2 = b + s1;
byte b3 = 2;
byte b4 = b + b3; // 报错5:有byte参与运算,结果自动提升为int型,byte不能兼容
boolean pass = true;
int i2 = pass; // 报错6:boolean不参与类型的自动转换
byte b5 = 1;
short s3 = 100;
int num200 = 2;
double num300 = 1.1;
int res = b5 + s3 + num200 + num300; // 报错7:表达式中最高精度为double型num300,结果为double型,int不能兼容
// 改进方法1;double res = b5 + s3 + num200 + num300;
// 改进方法2;float res = b5 + s3 + num200 + 1.1F
// 改进方法3;double res = b5 + s3 + num200 + 1.1F;
}
}
强制类型转换
自动类型转换的逆过程。将容量大的数据类型转换为容量小的数据类型。使用时要加上强制转换符()
,但有可能造成精度降低或溢出
注意:
- 数据的从大——>小,用强转。
- 强转符号只针对于最近的操作数有效,往往会使用小括号提升优先级。
- char类型可以保存int型的常量值,但不能保存int型的变量,需要强转。
public class ForceCOnvert {
public static void main(String[] args) {
int n1 = (int)1.9;
SYstem.out.println("n1 = " + n1); // 精度损失
int n2 = 2000;
byte b1 = (byte)n2;
SYstem.out.println("b1 = " + b1); // 数据溢出:2000超出byte范围[-128, 127]
int x = (int)10 * 3.5 + 6 * 1.5; // 编译错误2:double ——> int不兼容,精度丢失
// 强转符号只针对于最近的操作数有效,往往会使用小括号提升优先级
SYstem.out.println(x);
// 改进方法:int x = (int)(10 * 3.5 + 6 * 1.5);
char c1 = 100; // 正确,char能保存int型常量值
int m = 100; // 正确
char c2 = m; // 错误3:char不能保存int型的变量
char c3 = (char)m; // 正确,char保存int型变量需要强转
System.out.println(c3); // 输出(100对应的字符)d
}
}
基本数据类型和String类型的转换
-
基本类型转String类型
语法: 将基本类型的值+“”即可
public class StringToBasic {
public static void main(String[] args) {
int n1 = 100;
float n2 = 1.1f;
double n3 = 3.14;
boolean b1 = true;
String str1 = n1 + "";
String str2 = n2 + "";
String str3 = n3 + "";
String str4 = b1 + "";
System.out.println(str1 + " " + str2 + " " + str3 + " " + str4);
}
}
-
String类型转基本数据类型
语法:通过基本类型的包装类调用parseXXX方法即可
注意:
- 在将String类型转换成基本数据类型时,要确保String类型能够转成有效的数据。
- 如果格式不正确,就会抛出异常,程序就会终止。
public class BasicToString { public static void main(String[] args) { String s5 = "123"; int num1 = Integer.parseInt(s5); double num2 = Double.parseDouble(s5); float num3 = Float.parseFloat(s5); long num4 = Long.parseLong(s5); byte num5 = Byte.parseByte(s5); boolean b = Boolean.parseBoolean("true"); short num6 = Short.parseShort(s5); // 将String类型转换成char类型,在字符串中将指定位置的字符传换成char char c = s5.charAt(0); String str = "hello"; int n1 = Integer.parseInt(str); // 抛出异常 System.out.println(n1); } }
3、运算符
3.1、运算符介绍
运算符是一种特殊的符号,用以表示数据的运算、赋值和比较等。
3.2、算术运算符
算数运算符(ArithmeticOperator)是对数值类型的变量进行运算。
运算符 | 运算 | 范例 | 结果 |
---|---|---|---|
$+$ | 正号 | +7 | 7 |
$-$ | 负号 | b = 11; -b | -11 |
$+$ | 加 | 9 + 9 | 18 |
$-$ | 减 | 10 - 5 | 5 |
$*$ | 乘 | 7 * 8 | 56 |
$/$ | 除 | 9 / 4 | 2 |
% | 取模(取余) | 10 % 3 | 1 |
++ | 自增(前) | a = 2; b = ++a; | a = 3; b = 3 |
++ | 自增(后) | a = 2; b = a++; | a = 3; b = 2 |
-- | 自减(前) | a= 2; b = --a; | a = 1; b = 1 |
-- | 自减(后) | a= 2; b = a--; | a = 1; b = 2 |
+ | 字符串连接 | “study ” + “java” | “study java” |
注意
-
除法结果类型为操作数的最高精度。
-
取模(%)本质:a % b = a - a / b * b。
-
a % b当a是小数时,公式:a % b = a - (int)a / b * b。
-
有小数的运算,运算的结果是近似值。
-
Java语言中
+
的使用:- 当左右两边都是数值型时,则作加法运算
- 当左右两边有一方为字符串,则作拼接运算
- 运算顺序:从左至右
public class ArithmeticOperator {
public static void main(String[] args){
System.out.println(10 / 4); // 2
System.out.println(10.0 / 4); // 2.5
// 取模(取余)
System.out.println(10 % 3); // 1 = 10 - 10 / 3 * 3
System.out.println(-10 % 3); // -1 = (-10) - (-10) / 3 * 3
System.out.println(10 % -3); // 1 = 10 - 10 / (-3) * (-3)
System.out.println(-10 % -3); // -1 = (-10) - (-10) / (-3) * (-3)
System.out.println(-10.5 % 3); // -1.5 = -10.5 - (int)(-10.5) / 3 * 3, 结果为-1.5的近似值
System.out.println(-10.4 % 3); // -1.4 = -10.4 - (int)(-10.4) / 3 * 3, 结果为-1.4的近似值
}
}
3.3、关系运算符
关系运算符(RelationalOperator)的结果都是boolean
型,要么是true
,要么是false
。
运算符 | 运算 | 范例 | 结果 |
---|---|---|---|
$==$ | 相等于 | $8 == 7$ | false |
$!=$ | 不等于 | $8 != 7$ | true |
$<$ | 小于 | $8 < 7$ | false |
$>$ | 大于 | $8 > 7$ | true |
$<=$ | 小于等于 | $8 <= 7$ | false |
$>=$ | 大于等于 | $8 >= 7$ | true |
instance of |
检查是否是类的对象 | “yxz” instance of String | true |
public class RelationalOperator {
public static void main(String[] args) {
int a = 9;
int b = 8;
System.out.println(a > b);
System.out.println(a < b);
System.out.println(a >= b);
System.out.println(a <= b);
System.out.println(a == b);
System.out.println(a != b);
}
}
3.4、逻辑运算符
逻辑运算符(LogicalOperator)用于连接多个条件(多个关系表达式),最终的结果也是一个boolean值。
- 短路与
&&
,短路或||
,取反!
- 逻辑与
&
,逻辑或|
,逻辑异或^
| a | b | a&b
| a && b
| a | b
| a || b
| !a
| a ^ b
|
| :-----: | :-----: | :-----: | :------: | :-----: | :------: | :-----: | :-----: |
| true
| true
| true
| true
| true
| true
| false
| false
|
| true
| false
| false
| false
| true
| true
| false
| true
|
| false
| true
| false
| true
| true
| true
| true
| true
|
| false
| false
| false
| false
| false
| false
| true
| false
|
&&
和&
的区别
- 短路与
&&
:如果第一个条件为false
,则后面的条件都不会判断,最终结果为false
。 - 逻辑与
&
:不管第一个条件是否为false
,后面的条件都要判断,效率低。 - 开发中,使用的基本是短路与
&&
。
||
和|
的区别
- 短路与
||
:如果第一个条件为true
,则后面的条件都不会判断,最终结果为true
。 - 逻辑与
|
:不管第一个条件是否为true
,后面的条件都要判断,效率低。 - 开发中,使用的基本是短路与
||
。
public class LogicalOperator {
public static void main(String[] args) {
// &&短路与
int age = 50;
if(age > 20 && age < 90) {
System.out.println("ok100");
}
// &逻辑与
if(age > 20 & age < 40) {
System.out.println("ok200");
}
//区别
int a = 4;
int b = 9;
if(a < 1 && ++b < 50) {
System.out.println("ok300");
}
System.out.println("a = " + a + "b = " + b); // 输出a = 4,b = 9
if(a < 1 & ++b < 50) {
System.out.println("ok300");
}
System.out.println("a = " + a + "b = " + b); // 输出a = 4,b = 10
}
}
3.5、赋值运算符
赋值运算符(AssignOperator)就是将某个运算后的值,赋给指定的变量。
分类
- 基本赋值运算符
=
- 复合赋值运算符
+=
,-=
,*=
,/=
,%=
等。先运算后赋值
特点
- 运算顺序从右往左
- 赋值运算符的左边只能是变量,右边可以是变量、表达式、常量值。
复合赋值运算符会进行类型转换
。
public class AssignOperator {
public static void main(String[] args) {
int n1 = 10;
n1 += 4; // 等价于:n1 = n1 + 4;
System.out.println(n1); // 14
n1 /= 3; // 等价于:n1 = n1 / 3;
System.out.println(n1); // 4
// 复合赋值运算符会进行强制类型转换
byte b = 3;
b += 2; // 等价于:b = (byte)(b + 2);
b++; // 等价于:b = (byte)(b + 1)
}
}
3.6、三元运算符
三元运算符(TernaryOperator)基本语法:条件表达式 ? 表达式1 : 表达式2;
如果条件表达式为true
,运算后的结果是表达式1;如果条件表达式为false
,运算后的结果是表达式2.
表达式1和表达式2要为可以赋给接受变量的类型(或可以自动转换)。
三元运算符可以转成if-else
语句
public class TernaryOperator {
public static void main(String[] args) {
int a = 30;
int b = 50;
int result = a > b ? a++ : b--;
System.out.println(result) // 50
}
}
3.7、运算符的优先级
表达式运算中的运算顺序。如下表,上一行运算符总优先于下一行。
只有单目运算符、赋值运算符是从右向左运算的。
. () {} ; , |
|
---|---|
$R->L$ | ++ -- ~ ! |
$L->R$ | * / % |
$L->R$ | + - |
$L->R$ | << >> >>> 位移 |
$L->R$ | < > <= >= instanceof |
$L->R$ | == != |
$L->R$ | & |
$L->R$ | ^ |
$L->R$ | ` |
$L->R$ | && |
$L->R$ | ` |
$L->R$ | ? : |
$R->L$ | = *= /= %= |
+= -= <<= >>= |
|
>>>= &= ^= ` |
标识符的命名规则和规范
- Java中对各种变量、方法和类等命名时使用的字符序列成为标识符。
- 凡是可以自己起名字的地方都叫标识符。如:
int num1 = 90;
- 命名规则(必须遵守):
- 由``26个英文字母大小写
,
0-9,
_或
$`组成。 - 数字不可以开头。
- 不可以使用关键字和保留字,但能包含关键字和保留字。
- Java中严格区分大小写,长度无限制。
- 标识符不能含有空格。
- 由``26个英文字母大小写
标识符的规范
- 包名:多单词组成时所有字母都小写。如:
com.yxz.dao
- 类名、接口名:多单词组成时,所有单词的首字母大写(大驼峰)。如:
OperatorTest
- 变量名、方法名:多单词组成时,第一个单词首字母小写,第二个单词开始每个单词首字母大写(小驼峰,驼峰法)。如:
setNameBean
- 常量名:所有字母都大写。多单词时每个单词之间使用下划线连接。如:
LENGTH_MAXSIZE
- [Java开发手册(嵩山版).pdf](file:D:/学习文件/Java开发手册(嵩山版).pdf)
关键字和保留字
-
关键字:被Java语言赋予了特殊含义,用作专门用途的字符串
特点:关键字中所有字母都为小写。
-
保留字:现有Java版本尚未使用,但以后版本可能会作为关键字使用。命名标识符时要避免使用。
如:
byValue
、cast
、future
、generic
、inner
、operator
、outer
、rest
、var
、goto
、const
键盘输入
在编程中。需要接收用户输入的数据,可以使用键盘输入语句获取。需要一个扫描器(对象)
,就是Scanner类
。
步骤:
- 导入该类的所在包,
java.util.*
- 创建该类的对象(声明变量)
- 调用该类的成员方法
import java.util.Scanner;
public class Input {
public static void main(String[] args) {
// 1.导入Scanner类所在包
// 2.创建Scanner对象,使用关键字 new 创建一个对象
Scanner scanner = new Scanner(System.in);
// 3.接收用户输入,调用类的相关方法
System.out.println("请输入名字:");
String name = scanner.next(); // 等待接收键盘输入
System.out.println("请输入年龄:");
int age = scanner.nextInt(); // 等待接收键盘输入
System.out.println("请输入月薪:");
double salary = scanner.nextDouble(); // 等待接收键盘输入
System.out.println(name + "的个人信息");
System.out.println("年龄:" + age + "\t月薪:" + salary);
}
}
3.8、进制及转换
对于整数,有四种表示方式:
- 二进制:0,1。满2进1,以0b或0B开头。
- 十进制:0-9,满10进1。
- 八进制:0-7,满8进1,以数字0开头表示。
- 十六进制:0-9及A(10)-F(15),满16进1,以0x或0X开头表示。A-F不区分大小写。
public class BinaryTest {
public static void main(String[] args) {
// 二进制
int n1 = 0b1010; // 10
//十进制
int n2 = 1010; // 1010
// 八进制
int n3 = 01010; // 520
// 十六进制
int n4 = 0x10101; // 65793
}
}
进制转换
-
二进制转十进制
规则:从最低位(右边)开始,将每个位上的数提取出来,乘以2的(位数-1)次方,然后求和。
如:$0b1011$ ——> $120+1*21+022+1*23 = 11$
-
八进制转十进制
规则:从最低位(右边)开始,将每个位上的数提取出来,乘以8的(位数-1)次方,然后求和。
如:$0234$ ——> $480+3*81+28^2 = 156$
-
十六进制转十进制
规则:从最低位(右边)开始,将每个位上的数提取出来,乘以16的(位数-1)次方,然后求和。
如:$0X23A$ ——> $10160+3*161+216^2 = 570$
-
十进制转二进制
规则:将该数不断除以2,直到商为0为止,然后将每步得到的余数倒过来,就是对应的二进制。
如:34 ——> 0b100010 ——> 不足一个字节(8位), 高位补0 ——> 0b00100010
-
十进制转八进制
规则:将该数不断除以8,直到商为0为止,然后将每步得到的余数倒过来,就是对应的八进制。
如:131 ——> 0203
-
十进制转十六进制
规则:将该数不断除以16,直到商为0为止,然后将每步得到的余数倒过来,就是对应的八进制。
如:237 ——> 0xED
-
二进制转八进制
规则:从低位开始,将二进制数每三位一组,转换成对应的八进制数即可。
如:0b11010101 ——> 0b11(3)010(2)101(5) ——> 0325
-
二进制转十六进制
规则:从低位开始,将二进制数每四位一组,转换成对应的十六进制数即可。
如:0b11010101 ——> 0b1101(D)0101(5) ——> 0XD5
-
八进制转二进制
规则:将八进制数每1位,转成对应的一个3位的二进制数即可。
如:0237 ——> 0(0b)2(010)3(011)7(111) ——> 0b010011111
-
十六进制转二进制
规则:将八进制数每1位,转成对应的一个4位的二进制数即可。
如:0x23B ——> 0x2(0010)3(0011)B(1011) ——> 001000111011
3.9、位运算符
Java中的位运算符
符号名称 | 位运算符 | 范例 | 结果 |
---|---|---|---|
按位与 | & |
2 & 3 |
2 |
按位或 | ` | ` | `2 |
按位取反 | ~ |
~2 |
-3 |
按位异或 | ^ |
2 ^ 3 |
1 |
算数左移 | << |
8 << 2 |
32 |
算数右移 | >> |
4 >> 2 |
1 |
无符号右移(逻辑右移) | >>> |
16 >>> 2 |
4 |
运算规则:
- 算术右移
>>
:低位溢出,符号位不变,并用符号位补溢出的高位。 - 算数左移
<<
:符号位不变,低位补0。 - 逻辑右移
>>>
:也叫无符号右移。低位溢出,高位补0。
原码、反码、补码
对于有符号数:
- 二进制的最高位是符号位:0表示正数,1表示负数。
- 正数的原码、反码、补码都一样(正数三码合一)。
- 负数的反码 $=$ 它的原码符号位不变 $+$ 其他位按位取反(0->1, 1->0)。
- 负数的补码 $=$ 它的反码$+$1,负数的反码 $=$ 负数的补码 $-$ 1。
- 0的反码、补码都是0。
- Java中没有无符号数。
- 在计算机运算的时候都是以补码的方式运算的。
- 看运算结果时,要看它的原码。
public class BitOperator {
public static void main(String[] args) {
// 计算 2 & 3 = ?
// 1. 2的原码:00000000 00000000 00000000 00000010
// 2的补码:00000000 00000000 00000000 00000010(正数三码合一)
// 2. 3的原码:00000000 00000000 00000000 00000011
// 3的补码:00000000 00000000 00000000 00000011(正数三码合一)
// 3. 2的补码 & 3的补码
// 00000000 00000000 00000000 00000010(2的补码)
// 00000000 00000000 00000000 00000011(3的补码)
// 00000000 00000000 00000000 00000010 是运算后的补码
// 4. 运算后的原码:00000000 00000000 00000000 00000010(正数三码合一)
System.out.println(2 & 3); // 2 & 3 == 2
// 计算 ~-2
// 1. -2的原码:10000000 00000000 00000000 00000010(+2的原码基s础上,符号位改为1表示负数)
// 2. -2的反码:11111111 11111111 11111111 11111101(原码基础上, 符号位不变, 其他位按位取反)
// 3. -2的补码:11111111 11111111 11111111 11111110(反码基础上, +1)
// 4. ~-2操作:00000000 00000000 00000000 00000001 是运算后的补码
// 5. 运算后的原码: 00000000 00000000 00000000 00000001(正数三码合一)
System.out.println(~-2); // ~-2 == 1
// 计算 ~2
// 1. 2的补码:00000000 00000000 00000000 00000010(正数三码合一)
// 2. ~2操作 :11111111 11111111 11111111 11111101 是运算后的补码
// 3. 运算后的反码:11111111 11111111 11111111 11111100(运算后的补码基础上, -1)
// 4. 运算后的原码:10000000 00000000 00000000 00000011(运算后的反码基础上, 符号位不变, 其他位按位取反)
System.out.println(~-2); // ~2 == -3
}
}
4、控制结构
4.1、顺序控制
程序从上到下逐行地执行,中间没有任何判断和跳转。
Java中定义成员变量时采用合法的前向引用
(先定义后使用)。
4.2、分支控制
-
单分支if
基本语法:
if (条件表达式) { 代码块;(可以多条语句) }
说明:当条件表达式为
true
时,就会执行{}
内的代码。如果为false
,就不执行。如果{}
中只有一条语句,可以不写{}
,建议写上{}
-
双分支if-else
基本语法:
if (条件表达式) { 代码块1;(可以多条语句) } else { 代码块2;(可以多条语句) }
说明:当条件表达式为
true
时,就会执行代码块1
,否则执行代码块2
。 -
多分支if-else if
基本语法:
if (条件表达式1) { 代码块1;(可以多条语句) } else if (条件表达式2) { 代码块2;(可以多条语句) } else if (条件表达式3) { 代码块3;(可以多条语句) } ...... else { 代码块n;(可以多条语句) }
说明:当条件表达式1成立,则执行
代码块1
,如果不成立,判断条件表达式2,成立则执行代码块2
,否则判断条件表达式3...,都不成立则执行else内的代码块。只能有一个执行入口。多分支可以没有else,如果所有条件表达式都不成立,则一个执行入口都没有。 -
选择分支switch-case
基本语法:
switch (表达式) { case 常量1: 语句块1; break; case 常量2: 语句块2; break; ...... case 常量n: 语句块n; break; default: 语句块; break; }
说明:表达式对应一个值,当表达式的值等于常量1,就执行语句块1,break表示退出switch,不再继续匹配。如果一个都没匹配上,执行default语句块。
注意:
- 表达式数据类型,应和case后的常量类型一致,或者是可以自动转成可以相互比较的类型。
- switch(表达式)中表达式的返回值必须是:(byte、short、int、char、enum、String)
- case子句中的值必须是常量或者是常量表达式,不能是变量。
- default子句是可选的,当没有匹配的case时,执行default。
- break语句用来在执行完一个case分支后使程序跳出switch语句块;如果没有break,程序会顺序执行到switch结尾,除非遇到break。
4.3、循环控制
让一段代码可以循环重复的执行。
-
for循环
基本语法:
for (循环变量初始化; 循环条件; 循环变量迭代) { 循环体;(可以是多条语句) }
说明:
for
关键字,表示循环控制。- for有四要素:(1)循环变量初始化(2)循环条件(3)循环操作(4)循环变量迭代。
- 循环条件是返回一个boolean值的表达式。
for(; 循环判断条件; )
中的初始化和变量迭代可以写到其他地方,但是两边的;
不能省略。- 循环初始值可以有多条初始化语句,但是要求类型一样,并且中间用
,
隔开,循环变量迭代也可以有多条变量迭代语句,中间用,
隔开。
public class ForCircle { public static void main(String[] args) { int count = 3; for (int i = 0, j = 0; i < count; i++, j += 2) { System.out.println("i = " + i + "j = " + j); } int sum = 0; int i = 1; for (; i <= 100; ) { sum += i++; } System.out.println("sum = " + sum); for(; ; ) { // 死循环,配合break语句使用 } } }
练习:打印1~100之间所有9的倍数的整数,统计个数及总和。(思想:化繁为简、先死后活)
import java.util.Scanner; public class ForExercise { public static void main(String[] args) { // 化繁为简:即将复杂的需求,拆解成简单的需求,逐步完成。 // 先死后活:先考虑固定的值,然后转成可以灵活变化的值。 // 化繁为简 // 1. 完成 输出1-100的值 // 2. 输出过程中,进行过滤,只输出9的倍数 i % 9 == 0 // 3. 统计个数,定义一个变量 int count = 0; 当满足条件时,count++ // 4. 求和,定义一个变量 int sum = 0; 当满足条件时累加 sum += i int count = 0; for (int i = 1; i <= 100; i++) { if(i % 9 == 0) { System.out.println("i = " + i); count++; sum += i; } } System.out.println("sum = " + sum + ", count = " + count); // 先死后活 // 1. 将边界值设定为变量,输出[start, end]范围内的9的倍数 // 2. 将9的倍数改成变量x的倍数 // 3. 变量start、end、x使用Scanner来接收键盘输入 Scanner scanner = new Scanner(System.in); int start = scanner.nextInt(); int end = scanner.nextInt(); int x = scanner.nextInt(); int countX = 0; int sumX = 0; for (int i = start; i <= end; i++) { if(i % x == 0) { System.out.println("i = " + i); countX++; sumX += i; } } System.out.println("sumX = " + sumX + ", countX = " + countX); } }
-
while循环
基本语法:
循环变量初始化; while (循环条件) { 循环体;(可以是多条语句) 循环变量迭代; }
说明:while循环也有四要素,只是四要素放的位置和for循环不一样。
练习:打印1-100之间所有能被3整除的数。
public class WhileExercise { public static void main(String[] args) { int i = 1; int endNum = 100; while (i <= endNum) { if (i % 3 == 0) { System.out.println("i = " + i); } i++; } } }
-
do-while循环
基本语法:
循环变量初始化; do { 循环体; 循环变量迭代; } while (循环条件);
说明:
do while
是关键字。- do while也有循环四要素,只是位置不一样。
- 先执行一次,再判断是否循环。(至少会执行一次循环体)
- 最后有一个
;
。
-
嵌套循环(多层循环)
将一个循环放在另一个循环体内,形成嵌套循环。(建议一般使用两层,最多不超过三层,否则代码可读性很差)。
只有内层循环的循环条件为
false
时,才会完全跳出内循环,并结束外层循环的当次循环,开始外层循环的下一次循环。设外层循环次数
m
次,内层循环n
次,则内层循环体执行$n*m$次。练习:打印1-100中的所有素数。
public class MultiplyFor { public static void main(String[] args) { // 外循环,遍历2-100(1既不是素数,也不是合数) for(int j = 2; j <= 100; j++) { int i = 2; boolean isPrime = true; // 内循环,从2开始,循环条件i * i < j,找到j是否有除1和本身的其他因数 for(; i * i <= j; i++) { if(j % i == 0) { isPrime = false; break; } } if(isPrime) { System.out.println(j); } } } }
4.4、break
break语句用于终止某个语句块的执行,一般用在switch或者循环中。
注意:
-
break语句出现在多层嵌套的语句块中,可以通过标签指明要终止的是哪一层语句块。
-
标签的基本使用:
label1: {......循环体中: label2: {......循环体中: label3: {......循环体中: break label2; .... } } }
- break语句可以指定退出哪层。
- label1是标签,由程序员指定。
- break后指定到哪个label就退出哪层。
- 在实际的开发中,尽量不要使用标签。
- 如果没有指定break,默认退出最近的循环体。
public class Label { public static void main(String[] args) { abc1: for (int i = 0; i < 10; i++) { label2: for (int j = 0; j < 10; j++) { if (j == 2) { break abc1; } } } } }
4.5、continue
continue语句用于结束本次循环,继续执行下一次循环。
continue语句出现在多层嵌套的循环语句体中时,可以通过标签指明要跳过的是哪一层循环,和break使用方法一样。
基本语法:
{...循环体中:
continue;
...}
4.6、return
return使用在方法体里面,表示跳出所在的方法。如果return写在main()方法里面,退出程序。
public class Test {
public Test(){
}
public static void main(String[] args) {
Test test = new Test();
int sum = test.getSum(100);
System.out.println(sum);
}
public int getSum(int n) {
int sum = 0;
for(int i = 1; i < n; i++) {
sum += i;
}
return sum;
}
}
5、数组、排序和查找
5.1、数组
数组(Array)可以存放多个同一类型的数据。数组也是一种数据类型,属于引用类型。
基本语法:
// 静态初始化
数据类型[] 数组名 = {元素1, 元素2, 元素3,..., 元素n}; // 数组大小为n
int[] array01 = {1, 2, 3, 4, 5};
// 第一种动态分配方式
数据类型[] 数组名 = new 数据类型[数组大小];// java方式声明数组,建议使用
数据类型 数组名[] = new 数据类型[数组大小];// C语言方式声明数组,可以使用
double[] array02 = new double[10];
// 第二种动态分配方式,先声明,再new分配空间
数据类型[] 数组名; // 先声明(内存未给数组分配内存空间)
数组名 = new 数据类型[数组大小]; // 后实例化(给数组分配内存空间)
int[] a;
a = new int[10];
说明:
- 访问数组元素方法:数组名[下标]。下标(索引,index)从0开始编号,如:array[0]表示数组第一个元素,array[1]表示第二个元素,依此类推。
- 可以通过for就可以循环访问数组的元素/值。遍历数组就是指循环访问每一个数组元素。
数组名.length
可以获取数组的大小/长度。数组长度在第一次实例化时就已经固定不能再改变。- 数组是多个相同类型数据的组合,实现对同种数据的统一管理。
- 数组中的元素可以是任何数据类型,包括基本类型和引用类型,但不能混用。
- 数组创建后,如果没有赋值,有默认值。(
int 0, short 0, byte 0, long 0, float 0.0, double 0.0, char \u0000, boolean false, String null
) - 使用数组的步骤:1.声明数组并开辟空间 2.给数组各个元素赋值 3.使用数组。
- 数组下标(索引)必须在指定范围内使用,否则抛出异常:下标越界异常。
- 数组型数据是对象(object)。
数组赋值机制
-
基本数据类型赋值,这个值就是具体的数据,而且相互不影响。
-
数组赋值(ArrayAssign)在默认情况下是引用传递,赋的值是地址。
public class ArrayAssign { public static void main(String[] args) { // 基本数据类型赋值 -> 值传递 int n1 = 10; int n2 = n1; n1 = 80; // n1 = 80, n2 = 10 修改n1的值,不影响n2的值 // 数组类型赋值,默认是引用传递,赋的值是地址 -> 地址传递(引用传递) int[] arr1 = {1, 2, 3}; int[] arr2 = arr1; arr2[0] = 10; // 修改arr2的元素会影响到arr1 for (int i = 0; i < arr1.length; i++) { System.out.println(arr1[i] + " "); } } }
JVM内存图解(⭐⭐⭐⭐⭐)
二维数组
语法:
数据类型[][] 数组名 = new 数据类型[数组大小1][数组大小2];
// 数组大小1:表示有几个一维数组
// 数组大小2:表示每个一维数组的大小
一维数组中每个元素都是单独一个一维数组,就构成了二维数组。
其他数组的属性、操作类比一维数组即可。
注:
-
二维数组的声明方式有
数据类型[][] 数组名 = new 数据类型[数组大小1][数组大小2]; 数据类型 数组名[][] = new 数据类型[数组大小1][数组大小2]; 数据类型[] 数组名[] = new 数据类型[数组大小1][数组大小2];
-
二维数组的列数可以不等,即数组大小1$≠$数组大小2。如
int[][] map = {{1, 2}, {3, 4, 5}};
5.2、排序
排序是将一群数据,依指定的顺序进行排列的过程。
排序分类:
-
内部排序
指将需要处理的所有数据都加载到内部存储器中进行排序。包括:交换式排序法、选择式排序法、插入式排序法。
-
外部排序
数据量过大,无法全部加载到内存中,需要借助外部存储进行排序。包括:合并排序法、直接合并排序法。
冒泡排序(BubbleSort)
基本思想:通过对 待排序的序列从后向前
(从下标较大的元素开始),依次比较相邻元素的值
,若发现逆序则交换
,使值较大的元素逐渐从前往后移
。
public class BubbleSort {
public static void main(String[] args) {
// 化繁为简
int[] a = {4, 2, 5, 3, 1};
// 第一轮,目标:将第1大数放在倒数第1的位置上
System.out.print("第一轮:\n");
for(int j = 0; j < 4; j++) {
if(a[j] > a[j + 1]) {
int temp = a[j + 1];
a[j + 1] = a[j];
a[j] = temp;
}
}
// 输出排序后的数组
for (int i = 0; i < 5; i++) {
System.out.print(a[i] + "\t");
}
// 第二轮,目标:将第2大数放在倒数第2的位置上
System.out.print("\n第二轮:\n");
for(int j = 0; j < 3; j++) {
if(a[j] > a[j + 1]) {
int temp = a[j + 1];
a[j + 1] = a[j];
a[j] = temp;
}
}
// 输出排序后的数组
for (int i = 0; i < 5; i++) {
System.out.print(a[i] + "\t");
}
// 第三轮,目标:将第3大数放在倒数第3的位置上
System.out.print("\n第三轮:\n");
for(int j = 0; j < 2; j++) {
if(a[j] > a[j + 1]) {
int temp = a[j + 1];
a[j + 1] = a[j];
a[j] = temp;
}
}
// 输出排序后的数组
for (int i = 0; i < 5; i++) {
System.out.print(a[i] + "\t");
}
// 第四轮,目标:将第4大数放在倒数第4的位置上
System.out.print("\n第四轮:\n");
for(int j = 0; j < 1; j++) {
if(a[j] > a[j + 1]) {
int temp = a[j + 1];
a[j + 1] = a[j];
a[j] = temp;
}
}
// 输出排序后的数组
for (int i = 0; i < 5; i++) {
System.out.print(a[i] + "\t");
}
System.out.println("\n===============");
// 外层嵌套一层循环
for(int i = 0; i < 4; i++) {
System.out.print("第" + (i + 1) + "轮\n");
for(int j = 0; j < 4 - i; j++) {
if(a[j] > a[j + 1]) {
int temp = a[j + 1];
a[j + 1] = a[j];
a[j] = temp;
}
}
// 输出排序后的数组
for (int j = 0; j < 5; j++) {
System.out.print(a[j] + "\t");
}
System.out.print("\n");
}
int[] arr = {10, 4, 3, 6, 8, 2, 7, 1, 0, 5, 9};
// 先死后活,将数组长度改成arr.length
for(int i = 0; i < arr.length - 1; i++) {
for(int j = 0; j < arr.length - 1 - i; j++) {
if(arr[j] > arr[j + 1]) {
int temp = arr[j + 1];
arr[j + 1] = arr[j];
arr[j] = temp;
}
}
// 输出排序后的数组
for (int j = 0; j < arr.length; j++) {
System.out.print(arr[j] + "\t");
}
}
}
}
5.3、查找
Java中常用查找方法:
-
顺序查找(SequentialSearch)
import java.util.Scanner; public class SeqSearch { public static void main(String[] args) { String[] names = {"张三", "李四", "王五", "老六"}; Scanner scanner = new Scanner(System.in); System.out.print("请输入名字:"); String findName = scanner.next(); // 遍历数组,逐一比较 int index = -1; for (int i = 0; i < names.length; i++) { // 比较字符串 equals(String str) if (findName.equals(names[i])) { System.out.println("恭喜你找到 " + findName); System.out.println("下标为:" + i); index = i; // 保存下标 break; } } // 未找到则提示信息 if (index == -1) { System.out.println("sorry, 查无此人!"); } } }
-
二分查找(数据结构与算法中介绍)
6、面向对象编程(基础)
6.1、类与对象
类(class)是抽象的,概念的,代表一类事物,如:人类、猫类。即 类 ——> 数据类型
对象(object)是具体的,实际的,代表一个具体事物。即对象 ——> 实例
类是对象的模板,对象是类的一个个体,对应一个实例。
类包含属性(成员变量、字段、field)和行为(成员方法、类方法)。
属性(property)是类的一个组成部分,一般是基本数据类型,也可以是引用类型(对象、数组)
注意:
-
属性的定义语法和变量一致:
访问修饰符 属性类型 属性名;
,如:public String name;
Java一共有四种控制属性的访问范围,即以下四种访问修饰符:
public、protected、默认、private
。 -
属性的定义类型可以为任意类型,包含基本类型或引用类型。
-
属性如果不赋初值,有默认值,规则和数组一致。
JVM中对象的存储形式(⭐⭐⭐⭐⭐)
如何创建对象
-
先声明,再创建
基本语法:
类名 对象名; // 声明对象名,此时对象为空,没有创建对象空间 对象名 = new 类名(); // 创建对象(实例化)。new 会将创建的对象空间的地址返回赋给对象引用 Cat cat; // 声明对象名,此时cat为空,没有创建对象空间 cat = new Cat(); // 创建对象(实例化)。new 会将创建的对象空间的地址返回赋给cat
-
直接创建
基本语法:
类名 对象名 = new 类名(); // 声明对象名,同时创建对象空间并返回地址赋给对象名 Cat cat = new Cat(); // 类名Cat 对象引用(对象名)cat = 对象new Cat()
访问属性
基本语法:
对象名.属性名;
cat.age;
Java类和对象的内存分配机制
-
栈:一般存放基本数据类型(局部变量)
-
堆:存放对象(自定义类、数组等)
-
方法区:常量池(常量,比如字符串等),类加载信息(只加载一次)
public class Person { // 类属性:姓名 String name; // 类属性:年龄 int age; // 类属性:薪水 double salary; // 类属性:通过考试 boolean isPass; // 默认构造方法 public Person() { } // main()方法 public static void main(String[] args) { Person p = new Person(); // p是对象引用(对象名), new Person()创建的对象空间(包括数据)才是真正的对象 p.name = "Yancy"; p.age = 22; p.salary = 10000.0; p.isPass = false; /* 1、先加载Person类信息(属性和方法信息,只加载一次) 2、在堆中分配空间,进行默认初始化 3、把对象在堆中地址返回给p 4、进行指定初始化,如:p.name = "Yancy" */ } }
6.2、成员方法
成员方法是对象的行为。
基本语法:
访问修饰符 方法返回值类型 方法名(形参列表) {
方法体;
return 返回值;
}
说明:
- 访问修饰符:控制方法的使用范围。
- 方法返回值类型:表示成员方法的输出,
void
表示没有返回值。返回值类型可以为任意类型(基本类型、引用类型)。 - 方法名:遵循驼峰命名法,见名知意。
- 形参列表:表示成员方法的输入。用于传入参数,有值传递和地址传递。参数类型可以为任意类型(基本类型、引用类型)。
- 方法体:实现某一功能的代码块。语句可以为输入、输出、变量、运算、分支、循环、方法调用,但是里面不能再定义方法。即方法不能嵌套定义。
return
用于返回值,一个方法最多有一个返回值,返回值必须和返回值类型一致或兼容。如果是void
,可以只写return;
。- 一个方法可以有0个参数,也可以有多个参数,中间用
,
隔开。 - 调用带参数的方法时,一定对应着参数列表传入相同类型或兼容类型的参数。
- 方法定义时的参数称为形式参数,简称形参;方法调用时的参数称为实际参数,简称实参。实参和形参的类型要一致或兼容,个数和顺序必须一致!
- 同一个类中的方法调用:
直接调用
即可,无需借助对象名。 - 跨类中的方法调用:需要
通过对象名调用
。跨类方法调用和方法的访问修饰符有关
注:当程序执行到方法时,就会开辟一个独立的空间(栈空间)。
成员方法的好处:
- 提高代码的复用性。
- 可以将实现的细节封装起来,然后供其他用户来调用即可。
示例1:
public class Method01 {
String name;
// speak 成员方法
// public 表示方法公开
// void : 表示方法没有返回值
// speak() : speak是方法名,()形参列表
// {} 方法体 :执行代码块
public void speak() {
System.out.println("我是一个好人");
}
}
示例2:
public class Method01 {
String name;
public static void main(String[] args) {
Method01 method01 = new Method01();
method01.speak();
method01.cal01();
int returnRes = method01.cal02(100);
System.out.println("0~100的计算结果 = " + returnRes);
int[] res = method01.getSumAndSub(5, 3);
System.out.println("和 = " + res[0] + "差 = " + res[1]);
}
// speak 成员方法
// public 表示方法公开
// void : 表示方法没有返回值
// speak() : speak是方法名,()形参列表
// {} 方法体 :执行代码块
public void speak() {
System.out.println("我是一个好人");
B b = new B();
b.hi(); // 调用跨类方法需要借助对象名
}
public void cal01() {
int res = 0;
for (int i = 0; i <= 1000; i++){
res += i;
}
System.out.println("0~1000的计算结果 = " + res);
}
public int cal02(int n) {
speak(); // 同类方法可以直接调用,无需借助对象名
int res = 0;
for (int i = 0; i <= n; i++){
res += i;
}
return res;
}
public int[] getSumAndSub(int a, int b) {
int[] res = new int[2];
res[0] = a + b;
res[1] = a - b;
return res;
}
}
class B {
public void hi() {
System.out.println("B类中的hi()被执行");
}
}
6.3、方法传参机制
参考代码
public class Person {
public static void main(String[] args) {
Person p = new Person();
int res = p.getSum(10, 20);
System.out.println("getSum()的结果 = " + res);
}
public int getSum(int num1, int num2) {
int res = num1 + num2;
return res;
}
}
传参机制图解(⭐⭐⭐⭐⭐)
- 对于基本数据类型,参数传递的是值(值拷贝、值传递),形参在方法体中的的任何改变不影响实参。实参传入到方法的参数列表后,内存空间会给方法开辟新的栈空间,此时会将传递的实参的值拷贝给方法体的形参。
- 对于引用数据类型,参数传递的是地址(地址传递),可以通过改变方法体中的形参而影响实参。
public class MethodParameter {
public static void main(String[] args) {
MethodParameter methodParameter = new MethodParameter();
int a = 10;
int b = 20;
methodParameter.swap(a, b); // 基本数据类型,传参机制 ——> 值拷贝(值传递)
System.out.println("a = " + a + "b = " + b); // a = 10, b = 20
B obj = new B();
int[] arr = {1, 211, 6};
obj.updateValue(arr); // 引用数据类型,传参机制 ——> 地址传递(引用传递)
// 遍历数组
System.out.println("main()方法的arr数组");
for(int i : arr) { // 加强for循环,遍历数组的常用操作。使用i接收数组arr中每个元素
System.out.print(i + "\t"); // 输出:985 211 6
}
System.out.println();
}
public void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
}
}
class B {
public void updateValue(int[] arr) {
arr[0] = 985; // 修改元素
// 遍历数组
System.out.println("B类 update方法的arr数组");
for(int i : arr) { // 加强for循环,遍历数组的常用操作。使用i接收数组arr中每个元素
System.out.print(i + "\t"); // 输出:985 211 6
}
System.out.println();
}
}
克隆对象
通过地址传递的方法,克隆对象。
示例:
public class Person {
String name;
int age;
public static void main(String[] args) {
Person p = new Person();
p.name = "milan";
p.age = 22;
MyTools tools = new MyTools();
Person p2 = tools.copyPerson(p); // p2的属性和p的属性相同,但是两个对象不等
System.out.println("p的名字:" + p.name + ", p的年龄:" + p.age);
System.out.println("p2的名字:" + p2.name + ", p2的年龄:" + p2.age);
// 检验两个对象是否相等(属性相等 + 对象空间地址相等)
// 方法一:==判断
if(p == p2) {
System.out.println("p和p2相等");
} else {
System.out.println("p和p2不等");
}
// 方法二:使用object自带的equals()方法判断
if(p.equals(p2)) {
System.out.println("p和p2相等");
} else {
System.out.println("p和p2不等");
}
}
}
class MyTools {
public Person copyPerson(Person p) {
Person p2 = new Person();
p2.name = p.name;
p2.age = p.age;
return p2;
}
}
6.4、方法递归调用
递归(recursion)就是方法自己调用自己,每次调用时传入不同的变量。递归有助于编程解决复杂问题,可以让代码更简洁。
递归能解决什么问题?
- 各种数学问题:8皇后问题,汉诺塔,阶乘(factorial)问题,迷宫问题,球和篮子的问题 等等。
- 各种算法中用到递归:快速排序,归并排序,二分查找,分治算法 等等。
- 用栈解决的问题 ——> 递归代码更简洁。
示例:
public class Recursion {
T t = new T();
t.test(4); // 输出:n = 2 n = 3 n = 4
int res = t.factorial(5);
System.out.println("res = " + res); // 输出:res = 120
}
class T {
public void test(int n) {
if (n > 2) {
test(n - 1);
}
System.out.println("n = " + n);
}
// 计算n的阶乘n!
public int factorial(int n) {
if(n == 1) {
return 1;
} else {
return factorial(n - 1) * n;
}
}
}
递归重要规则
- 执行一个方法时,就创建一个新的独立空间(栈空间)。
- 方法的局部变量是独立的,不会相互影响。
- 如果方法中使用的是引用类型变量,就会共享该引用类型的数据。
- 递归三要素:递归函数的参数和返回值、递归终止条件、递归的等价关系式。递归必须向退出递归的条件逼近,否则会无限递归,抛出异常(
StackOverFlowError
, 栈溢出)。 - 当一个方法执行完毕,或者遇到
return
,就会返回,遵守谁调用,就将结果返回给谁。当方法执行完毕或返回时,方法占用的内存空间会被收回或释放。
迷宫问题(递归解决)
已知老鼠在地图最右上角,地图上有若干障碍物,现在需要找到一条路径使得老鼠能够走到地图右下角。规定老鼠每次只能走一格。
进阶思考:一共有多少条不同的路径数?最短路径?迷宫足够大时,递归爆栈,算法如何优化?
public class MiGong {
public static void main(String[] args) {
// 1. 先创建迷宫,用二维数组表示 int[][] map = new int[8][7];
int[][] map = new int[8][7];
// 2. 规定map数组的元素值: 0表示可以走,1表示障碍物
// 3. 最上面的一行和最下面的一行设置为1
for (int i = 0; i < 7; i++) {
map[0][i] = 1;
map[7][i] = 1;
}
// 4.最左边的一列和最右边的一列设置为1
for (int i = 0; i < 8; i++) {
map[i][0] = 1;
map[i][6] = 1;
}
// 5. 障碍物的位置设为1
map[3][1] = 1;
map[3][2] = 1;
map[2][2] = 1;
// 输出当前地图
System.out.println("==========当前地图情况===========");
for (int[] value : map) {
for (int i : value) {
System.out.print(i + " ");
}
System.out.println();
}
// 使用findWay找路
Solution solution = new Solution();
solution.findWay(map, 1, 1); // 传入老鼠的初始位置
System.out.println("\n=======找路的情况如下=========");
for (int[] ints : map) {
for (int anInt : ints) {
System.out.print(anInt + " ");
}
System.out.println();
}
}
}
class Solution {
// 1. findWay()方法找到出迷宫的路径
// 2. 如果找到,返回true,否则返回false
// 3. 二维数组map表示迷宫
// 4. i, j 就是老鼠的位置,初始化位置为(1, 1)
// 5. 递归找路,规定map数组各个值的含义:
// 0表示可以走,1表示障碍物,2表示已走路线,3表示走过但是走不通
// 6. 当map[6][5] = 2,说明找到通路就可以退出,否则继续找
// 7. 先确定老鼠找路的策略:下 -> 右 -> 上 -> 左(策略不唯一 ——> 找到的路径不唯一)
public boolean findWay(int[][] map, int i, int j) {
if (map[6][5] == 2) { // 说明已经找到,返回true
return true;
} else {
if (map[i][j] == 0) { // 当前位置为0,表示可以走
// 假定当前位置可以走通
map[i][j] = 2;
// 使用找路策略,来确定当前位置可以走通:下 -> 右 -> 上 -> 左
if (findWay(map, i + 1, j)) { // 先走下
return true;
} else if (findWay(map, i, j + 1)) { // 右
return true;
} else if (findWay(map, i - 1, j)) { // 上
return true;
} else if (findWay(map, i, j - 1)) { // 左
return true;
} else {
map[i][j] = 3; // 表示当前位置走过但是走不通
return false;
}
} else { // map[i][j] = 1, 2, 3
return false; // 其他情况,返回false
}
}
}
}
汉诺塔问题
有三根柱子,在一个柱子上从下往上按照从大到小顺序摞着n个圆盘。现在要把这n个圆盘按照大小顺序重新摆放在另一根柱子上,并且规定在小圆盘上不能放大圆盘,且三根柱子之间一次只能移动一格圆盘。
public class HanoiTower {
public static void main(String[] args) {
Tower solve = new Tower();
solve.move(5, 'A', 'B', 'C');
}
}
class Tower {
// move()方法表示移动圆盘的操作
// num: 要移动的圆盘个数
// a, b, c: 表示三根柱子A塔,B塔,C塔
public void move(int num, char a, char b, char c) {
// 如果只有一个盘num = 1
if (num == 1) {
System.out.println(a + "->" + c);
} else {
// 如果有多个盘,可以看成两块,最下面的一块盘和上面的所有盘(num - 1)
// 1. 先移动上面所有的盘到b, 借助c
move(num - 1, a, c, b);
// 2. 把最下面的盘移动到c
System.out.println(a + "->" + c);
// 3. 再把b塔的所有盘移动到c,借助a
move(num - 1, b, a, c);
}
}
}
八皇后问题(回溯算法的典型)
在 $8×8$ 的国际象棋上摆放八个皇后,使其不能互相攻击。即:任意两个皇后都不能处于同一行、同一列或同一斜线上,共有多少种摆法?
思路分析:
-
第一个皇后先放在第一行第一列。
-
第二个皇后放在第二行第一列,然后判断是否可行,如果不可行,继续放在第二列、第三列,依次把所有列都放完直到找到一个合适的方案。
-
继续第三个皇后,以此类推,直到第八个皇后也能放在合理的位置,则找到一个正确解。
-
当得到一个正确解时,在栈回退到上一个栈时,就会开始回溯,即将第一个皇后,放在第一列的所有正确解,全部得到。
-
然后回头继续第一个皇后放第二列,继续循环1-4的步骤
注:理论上要创建二维数组表示棋盘,实际可以通过算法,用一个一维数组解决。
arr[8] = {0, 4, 7, 5, 2, 6, 1, 3}
对应arr下标
表示第几行,即第几个皇后,arr[i] = val
表示第i + 1
个皇后,放在第i + 1
行的第val + 1
列。
6.5、方法重载
Java中允许同一个类
中,多个同名方法
的存在,但要求形参列表不一致
,这种现象称为方法重载(overload)
。
如:System.out.println();
out
是PrintStream
类型的对象
。
方法重载注意:
- 方法名必须相同。
- 参数列表必须不同(1.形参类型不同 2.形参个数不同 3.形参顺序不同 ——> 至少一样不同),与参数名无关。
- 与方法返回类型无关。
- 当传入的实参与多个重载方法匹配时(出现自动类型转换),
优先调用无自动类型转换的方法
。
public class OverLoad {
public static void main(String[] args) {
MyCalculator mc = new MyCalculator();
System.out.println(mc.calculate(1, 2)); // 优先调用无需转换类型的方法calculate(int, int)
System.out.println(mc.calculate(1, 3.14));
System.out.println(mc.calculate(5.2, 0));
}
}
class MyCalculator {
// 下面四种不同calculate()方法构成了方法的重载
public int calculate(int n1, int n2) {
return n1 + n2;
}
public double calculate(int n1, double n2) {
return n1 + n2;
}
public double calculate(double n1, int n2) {
return n1 + n2;
}
public int calculate(int n1, int n2, int n3) {
return n1 + n2 + n3;
}
}
6.6、可变参数
Java允许将同一个类
中多个同名同功能
但参数个数不同
的方法,封装成一个方法。可以通过可变参数(Variable parameters)实现。
基本语法:
访问修饰符 返回类型 方法名(数据类型...形参名) {
方法体;
}
案例:类Method,方法sum(可以计算 2个数的和,3个数的和,4.5。。。)。
public class VarParameter {
public static void main(String[] args) {
Method method = new Method();
int res = method.sum("variable parameter", 1, 2, 3, 4, 5, 6);
System.out.println(res);
int[] arr = {985, 211, 520};
int result = method.sum("传递数组", arr);
System.out.println("传递arr数组求和 = " + result);
System.out.println(method.calScore("Mi Lan", 99.0, 92, 87, 95.5, 90));
}
}
class Method {
// 可以使用方法重载实现
/*
public int sum(int n1, int n2) {
return n1 + n2;
}
public int sum(int n1, int n2, int n3) {
return n1 + n2 + n3;
}
public int sum(int n1, int n2, int n3, int n4) {
return n1 + n2 + n3 + n4;
}
*/
// ......
// 以上三个方法名相同,功能相同,形参个数不同 -> 使用可变参数优化
// int...: 表示接收的是可变参数,类型是int,即可以接收多个int(0——>n),本质上是个数组
// 遍历nums求和
public int sum(String str, int... nums) {
System.out.println("接收的参数个数 = " + nums.length);
int res = 0;
for (int i = 0; i < nums.length; i++) {
res += nums[i];
}
return res;
}
public String calScore(String name, double... scores) {
double totalScore = 0;
for (int i = 0; i < scores.length; i++) {
totalScore += scores[i];
}
return name + "有" + scores.length + "门课的成绩总分为" + totalScore;
}
}
注意:
- 可变参数的实参可以为0个或任意多个。
- 可变参数的实参可以为数组。
- 可变参数的本质就是可变长度的数组。
- 可变参数可以和普通类型的参数一起放在形参列表,但必须保证可变参数在形参列表最后。
- 一个形参列表中只能出现一个可变参数。
6.7、作用域
Java中主要变量就是属性(成员变量)和局部变量。
局部变量一般是指在成员方法中定义的变量。
Java中作用域的分类:
- 全局变量:也就是属性,作用域为整个整体。可以被本类使用,或被其它类通过对象调用。
- 局部变量:也就是除了属性之外的其他变量,作用域为从定义它的语句开始到代码块执行结束。只能在本类中对应的方法中使用。
全局变量可以不赋值,直接使用,因为有默认值。局部基本数据类型变量(没有默认值)必须赋值后才能使用。
注意:
-
属性和局部变量可以重名,访问时遵循就近原则(在方法中与属性同名的局部变量会暂时覆盖属性,要访问属性需要借助
this
或者对象名)。 -
在同一个作用域中,变量不能重名。如在同一个成员方法中,两个局部变量不能重名。
-
属性生命周期较长,伴随着对象的创建而创建,伴随着对象的死亡而死亡。局部变量生命周期较短,伴随着它的定义语句执行而创建,伴随着代码块的结束而死亡。
-
全局变量可以加修饰符,局部变量不可以加修饰符
public class VarScope { String name; // 默认值为null public int age; // 属性可以加修饰符 public static void main(String[] args) { VarScope varscope = new VarScope(); varscope.hi(); } // 对象varscope伴随着程序终止而被销毁 public void hi() { int num; // 此时num没有值,直接输出会报未初始化赋值的错误 // int num; // 同一个作用域中,变量不能重名 num = 10; System.out.println(num); String name = "Mi Lan"; // 与属性同名,在这个方法体内,局部变量name覆盖属性name System.out.println(name); System.out.println(this.name); // 通过this关键字可以访问该类属性name int res = 0; // 局部变量不能加修饰符 for (int i = 0; i < 100; i++) { // 局部变量 i 的作用域伴随着for循环结束而终止。 res += i; }// 此处退出循环后,局部变量i立即销毁 } }
6.8、构造器
构造器(构造方法, constructor)是类的一种特殊方法,它的主要作用是完成对新对象的初始化。在创建对象时,系统会自动的调用该类的构造器完成对该对象的初始化。
基本语法:
[修饰符] 方法名(形参列表) {
方法体;
}
说明:
- 构造器的修饰符可以默认,也可以为
public
、private
、protected
。 - 构造器没有返回值类型,也不能写
void
。 - 构造方法的方法名和类名必须一样。
- 参数列表和其他成员方法一样的规则。
- 构造器的调用由系统完成。
- 如果不定义构造器,系统会给类创建一个默认无参构造器。一旦定义了有参构造器,就会覆盖默认无参构造器,无法再使用无参构造,除非再显式定义一个无参构造器。
- 一个类可以定义多个不同的构造器,即构造器重载。
- 构造器的任务是完成对象属性的初始化,而不是创建对象。
public class Constructor {
public static void main(String[] args) {
Person p1 = new Person("Mi Lan", 22);
System.out.println("p1对象name: " + p1.name + ", 芳龄: " + p1.age);
}
}
class Person {
String name;
int age;
// 构造器
public Person(String pName, int pAge) {
name = pName;
age = pAge;
}
}
class Dog {
String name;
int age;
/*默认构造器,如果未定义有参构造器,则系统自动创建默认构造器。可以使用javap反编译显示出来
Dog() {
}
*/
Dog(String dogName, int dogAge) { // 有参构造器,会覆盖掉默认构造器(此时无法调用无参构造器)
}
// 显式定义无参构造器
Dog() {
}
}
反编译工具
使用javap
指令能对给定的.class
文件提供的字节码进行反编译成.java
文件。
通过javap
指令,可以对照源代码和字节码,从而了解很多编译器内部的工作,对更深入的理解如何提高程序执行的效率等问题有很大帮助。
基本语法:
javap <options> <classes>
常用:javap -c -v 类名
javap常用指令 | 含义 |
---|---|
-help --help -? |
输出此用法消息 |
-version |
版本消息 |
-v -verbose |
输出附加信息 |
-l |
输出行号和本地变量表 |
-public |
仅显示公共类和成员 |
-protected |
显示受保护的/公共类和成员 |
-package |
显示程序包/受保护的/公共类和成员(默认) |
-p -private |
显示所有类和成员 |
-c |
对代码进行反汇编 |
-s |
输出内部类型签名 |
-sysinfo |
显示正在处理的类的系统信息(路径, 大小, 日期, MD5 散列) |
-constants |
显示最终常量 |
-classpath <path> |
指定查找用户类文件的位置 |
-cp <path> |
指定查找用户类文件的位置 |
-bootclasspath <path> |
覆盖引导类文件的位置 |
创建对象的流程分析
案例代码
class Person {
int age = 18;
String name;
Person(String pName, int pAge) { // 构造器
name = pName; // 给属性赋值
age = pAge;
}
public static void main(String[] args) {
Person p = new Person("Mi Lan", 22);
}
}
流程分析(面试题)
- 加载Person类信息(
Person.class
),只会加载一次。 - 使用
new
给对象在堆中分配空间。 - 完成对象初始化
- 默认初始化。给对象的属性根据数据类型赋默认值。如:name = null,age = 0;
- 显式初始化。类属性是否初始化。如上面代码中
int age = 18;
。 - 构造器的初始化。将构造器的实参赋给对象属性。如上面代码中
new Person("Mi Lan", 22);
- 将对象在堆中的地址返回给对象引用(对象名)。如上面代码中
Person p = new Person("Mi Lan", 22);
6.9、this
Java虚拟机(JVM)会给每个对象分配this
,代表当前对象
。可以理解成,当new给对象在堆中分配空间时,会自动给该对象分配一个“隐藏的属性”——this
,这个this类似C语言中的指针,只是这个“指针”永远指向包含该“指针”的对象。
简单来说:哪个对象调用,this
就代表哪个对象。
public class UseThis {
public static void main(String[] args) {
Dog dog1 = new Dog("大壮", 3);
System.out.println("dog1的hashCode = " + dog1.hashCode());// dog1的hashCode和构造器内部打印this的hashCode一致
dog1.printDogInfo(); // 打印的hashCode和dog1的hashCode一致
Dog dog2 = new Dog("大黄", 2);
System.out.println("dog2的hashCode = " + dog2.hashCode());// dog2的hashCode和构造器内部打印this的hashCode一致
dog1.printDogInfo(); // 打印的hashCode和dog2的hashCode一致
// 结论:this指代当前对象
}
}
class Dog {
public String name;
public int age;
public Dog(String name, int age) {
// this.name 就是当前对象的属性name
this.name = name;
// this.age 就是当前对象的属性age
this.age = age;
// hashCode():返回该对象的哈希码值
// hashCode会根据在内存中不同对象的地址映射成不同的整数
System.out.println("this.hashCode = " + this.hashCode());
}
public void printDogInfo() {// 打印狗的信息
System.out.println(name + "\t" + age + "\t");
System.out.println("Info方法中this.hashCode = " + this.hashCode());
}
}
this
关键字的本质(⭐⭐⭐⭐)
注意:
this
关键字可以用来访问本类的属性、方法、构造器。this
用于区分当前类的属性和局部变量。- 访问成员方法的语法:
this.方法名(参数列表);
- 访问构造器的语法:
this(参数列表);
注意只能在构造器中使用,即只能在构造器中使用this
调用另外一个构造器。 this
不能在类定义的外部使用,只能在类定义的方法中使用。- 如果要在构造器中使用
this
访问另外一个构造器,则this(参数列表);
这个语句必须是构造器中的第一个语句。
class ThisDetail {
public static void main(String[] args) {
T t1 = new T("张三", 30);
t1.f2();
T t2 = new T();
t2.f3();
}
}
class T {
public String name = "smith";
public int age = 18;
public T() {// 默认构造器
// 在默认构造器中访问有参构造器
this("Mi Cai", 23); // 使用this访问另一个构造器,必须是构造器中的第一个语句
System.out.println("T()构造器");
}
public T(String name, int age) {// 有参构造器
this.name = name;
this.age = age;
System.out.println("T(String name, int age)构造器");
}
public void f1() {
System.out.println("f1()方法...");
}
public void f2() {
System.out.println("f2()方法...");
// 调用本类的f1方法
// 第一种方式
f1();
// 第二种方式,与第一种方式有区别(继承介绍)
this.f1();
}
public void f3() {
String name = "Mi Lan";
// 就近原则,name指代局部变量name,age指代属性age
System.out.println("name = " + name + ", age = " + age); // Mi Lan, 18
// 使用this访问当前对象的属性
System.out.println("name = " + this.name + ", age = " + this.age); // smith, 18
}
}
6.10、实战运用
人机猜拳游戏
import java.util.Random;
import java.util.Scanner;
/*
人机猜拳游戏
电脑每次随机生成0, 1, 2
0 表示石头 1表示剪刀 2表示布
并统计人的输赢次数
*/
public class MoraGame {
public static void main(String[] args) {
System.out.println("猜拳游戏\n0 表示石头 1表示剪刀 2表示布");
// 创建一个玩家对象
Player player = new Player();
// 声明一个变量统计输赢的次数
int isWinCount = 0;
// 创建一个二维数组,接收局数,玩家出拳以及电脑出拳的情况
int[][] arr1 = new int[3][3];
int j = 0;
// 创建一个一维数组,用来接收输赢的情况
String[] arr2 = new String[3];
Scanner scanner = new Scanner(System.in);
for (int i = 0; i < 3; i++) {
// 获取玩家出拳
System.out.println("请输入你要出的拳(0-石头, 1-剪刀, 2-布):");
int num = scanner.nextInt();
player.setPlayerGuessNum(num);
int playerGuess = player.getPlayerGuessNum();
arr1[i][j + 1] = playerGuess;
// 获取电脑出拳
int computerGuess = player.setComputerGuessNum();
arr1[i][j + 2] = computerGuess;
// 将玩家猜的拳和电脑比较
String isWin = player.vsComputer();
arr2[i] = isWin;
arr1[i][j] = player.count;
// 对每一局的情况输出
System.out.println("=====================================");
System.out.println("局数\t玩家的出拳\t电脑的出拳\t输赢情况");
System.out.println(player.count + "\t\t" + playerGuess + "\t\t\t" + computerGuess + "\t\t\t" + isWin);
System.out.println("=====================================");
System.out.println("\n");
isWinCount = player.winCount(isWin);
}
// 对游戏的最终结果进行输出
System.out.println("局数\t玩家的出拳\t电脑的出拳\t\t输赢情况");
int a;
for (a = 0; a < arr1.length; a++) {
for (int b = 0; b < arr1[a].length; b++) {
System.out.print(arr1[a][b] + "\t\t\t");
}
System.out.print(arr2[a]);
System.out.println();
}
System.out.println("你赢了" + isWinCount + "次");
}
}
class Player {
// 玩家出拳的类型
int playerGuessNum;
// 电脑出拳的类型
int computerGuessNum;
// 玩家赢的次数
int winCount;
// 比赛的总场数
int count = 1;
/**
* 设置电脑随机生成猜拳的数字的方法
*
* @return :返回电脑猜拳的数字
*/
public int setComputerGuessNum() {
Random r = new Random();
this.computerGuessNum = r.nextInt(3);
return computerGuessNum;
}
/**
* 获取玩家猜拳的数字的方法
*
* @return :返回玩家猜拳的数字
*/
public int getPlayerGuessNum() {
return playerGuessNum;
}
/**
* 设置玩家猜拳的方法
*
* @param playerGuessNum :玩家猜拳的数字
*/
public void setPlayerGuessNum(int playerGuessNum) {
if (playerGuessNum > 2 || playerGuessNum < 0) {
throw new IllegalArgumentException("数字输入错误");
}
this.playerGuessNum = playerGuessNum;
}
/**
* 统计胜场次数
*
* @param s :胜负情况
* @return :返回胜场次数
*/
public int winCount(String s) {
count++; // 总场数自增
if (s.equals("你赢了")) {
winCount++;
}
return winCount;
}
/**
* 胜负判断
*
* @return :返回胜负情况
*/
public String vsComputer() {
if (playerGuessNum == 0 && computerGuessNum == 1) {// 人:石头,机:剪刀
return "你赢了";
} else if (playerGuessNum == 1 && computerGuessNum == 2) {// 人:剪刀,机:布
return "你赢了";
} else if (playerGuessNum == 2 && computerGuessNum == 0) {// 人:布,机:石头
return "你赢了";
} else if (playerGuessNum == computerGuessNum) {// 一样
return "打平手";
} else {
return "你输了";
}
}
}
IDEA
-
IDEA
介绍IDEA
全称IntelliJ IDEA
。在业界被公认为最好的Java开发工具。
IDEA是JetBrains公司的产品。
除了支持Java开发,还支持HTML,CSS,PHP,MySQL,Python等。
-
安装:官网
-
IDEA界面
上边——菜单区,左边——项目导航区,右边——代码编辑区,下边——输出的控制台区
-
设置字体:
- 外观和行为
Appearance & Behavior
->Appearance
- 文件
file
->settings
->Editor
->Font
- 外观和行为
-
在IDEA中,使用
run
一个.java
文件时,会把该文件编译成.class
字节码文件,然后再运行。编译生成的
.class
文件会自动在项目目录下的out文件夹
-
IDEA常用快捷键
修改快捷键:文件
file
->settings
->Keymap
->Editor Actions
- 删除当前行:
Ctrl + D
- 快速格式化代码:
Ctrl + Shift + F
- 快速运行程序:
Alt + R
。如果需要使用快捷键运行,需要先配置主类
。第一次运行需要鼠标右键Run
。 - 生成构造器等:
Alt + Insert
- 查看一个类的层级关系:
Ctrl + H
- 将光标放在一个方法上,定位到哪个类的方法:
Ctrl + B
- 删除当前行:
-
模板:文件
file
->settings
->Editor
->Live templates
7、面向对象编程(中级)
7.1、包
作用:
- 区分很多相同名字的类。
- 当类很多时,可以很好的管理类。
- 控制访问范围。
基本语法:
package 包名;
package
关键字,表示打包。
包的本质分析(原理):实际上就是创建不同的文件夹来保存类文件
包的命名规则:只能包含数字、字母、下划线、小圆点.
,但不能用数字开头,不能是关键字或保留字
包的命名规范:一般是小写字母+小圆点.
。一般是com.公司名firm.项目名project.业务模块名model
。
com.sina.crm.user// 用户模块
com.sina.crm.order// 订单模块
com.sina.crm.utils// 工具类
Java中常用的包(.*
表示所有包):
java.lang.*
:lang包是基本包,默认引入,不需要再引入java.util.*
:util包,系统提供的工具包,工具类java.net.*
:网络包,网络开发java.awt.*
:Java图形化界面开发(GUI)的包
注意:
package 包名;
的声明必须放在类的最上面,一个类中最多一句package
。import
指令位置放在package
下面,在类定义前面。
7.2、访问修饰符
Java提供四种访问控制修饰符号,用于控制方法和属性(成员变量)的访问权限(范围):
- 公开级别:用
public
修饰,对整个项目所有包中的类
公开。 - 受保护级别:用
protected
修饰,对子类
和同一个包中的类
公开。 - 默认级别:没有修饰符号,只向
同一个包中的类
公开。 - 私有级别:用
private
修饰,只有类本身可以访问
,不对外公开。
访问修饰符的权限图(⭐⭐⭐⭐⭐)
注意:
- 修饰符可以用来修饰类中的属性,成员方法以及类。
- 只有默认的和
public
才能修饰类,并且遵循以上访问权限的特点。 - 成员方法的访问规则和属性完全一样。
- 默认级别的子类对象不能访问不同包下父类受保护级别的方法或属性。
com.yxz.modifier.A.java
package com.yxz.modifier;
public class A {
// 四个属性,分别使用不同的访问修饰符来修饰
public int n1 = 100;
protected int n2 = 200;
int n3 = 300;
private int n4 = 400;
public void m1() {
// 同一个类中可以访问public、protected、默认、private修饰的属性或方法
System.out.println("n1 = " + n1 + ", n2 = " + n2 + ", n3 = " + n3 + ", n4 = " + n4);
}
protected void m2() {
}
void m3() {
}
private void m4() {
}
public void say() {
// 同一个类中可以访问public、protected、默认、private修饰的属性或方法
m1();
m2();
m3();
m4();
}
}
com.yxz.modifier.B.java
package com.yxz.modifier;
public class B {
public void hi() {
A a = new A();
// 在同一个包下,可以访问 public,protected和默认修饰属性或方法,不能访问private
System.out.println("n1 = " + a.n1 + ", n2 = " + a.n2 + ", n3 = " + a.n3);
a.m1();
a.m2();
a.m3();
// a.m4(); 错误:不同类中无法访问private修饰的方法
}
}
com.yxz.modifier.Test.java
package com.yxz.modifier;
public class Test {
public static void main(String[] args) {
A a = new A();
a.m1();
B b = new B();
b.hi();
}
}
// 只有默认和public可以修饰类
class Tiger {
}
com.yxz.pkg.Test.java
package com.yxz.pkg;
import com.yxz.modifier.A;
public class Test {
public static void main(String[] args) {
A a = new A();
// 在不同包下,可以访问public修饰的属性或方法
// 但是不能访问protected、默认、private修饰的属性或方法
System.out.println(a.n1);
a.m1();
// 在不同包下,不能访问protected、默认、private修饰的属性或方法
// a.m2();
// a.m3();
// a.m4();
B b = new B();
// b.m2(); // 默认级别的子类对象不能访问不同包下父类受保护级别的方法
// System.out.println(b.n2); // 默认级别的子类对象不能访问不同包下父类受保护级别的属性
}
}
class B extends A {
}
面向对象编程(OOP, Object-oriented programming)三大特征:封装
、继承
、多态
。
7.3、封装
封装(encapsulation)就是把抽象出的数据(属性)和对数据的操作(方法)封装在一起,数据被保护在内部,程序的其他部分只有通过被授权的操作(方法)才能对数据进行操作。
封装的好处:
- 隐藏实现的细节,调用者无需关注内部运行原理。
- 可以对数据进行验证,保证数据的安全。
封装的实现步骤:
-
将属性进行私有化
private
(外部不能直接修改属性)。 -
提供一个公共的(
public
)set方法,用于对属性判断并赋值。public void setXxx(数据类型 参数名) {// Xxx表示某个属性 // 数据验证的业务逻辑 this.属性 = 参数名; }
-
提供一个公共的get方法,用于获取属性的值。
public 返回值类型 getXxx() {// Xxx表示某个属性 // 权限判断 return xxx; }
综合案例
编写一个类Person,属性(姓名,年龄,工作,薪水)
要求:外部不能随便查看人的年龄、工资等隐私
并对设置的年龄进行合理的验证,年龄合理就设置,否则给默认年龄。必须在1-120之间
工资不能直接查看,name的长度在2-6个字符。
package com.yxz.encap;
public class Encapsulation {
public static void main(String[] args) {
Person person = new Person();
// person.name = "Mi cai"; 名字长度需要判断是否合理
// person.age = 22; 错误:不同类之间不能访问私有属性
person.setName("米彩");
person.setAge(23);
person.setSalary(1000000.0);
person.setJob("总裁");
System.out.println(person.toString());
Person person1 = new Person("米兰", 2000, 300000.0, "经理");
System.out.println(person1.toString());
}
}
class Person {
public String name; // 名字公开
private int age; // 年龄私有化
private double salary; // 薪水 私有化
private String job;
public Person() {
}
public Person(String name, int age, double salary, String job) {
// this.name = name;
// this.age = age;
// this.salary = salary;
// this.job = job;
setName(name);
setAge(age);
setSalary(salary);
setJob(job);
}
public String getName() {
return name;
}
public void setName(String name) {
// 加入对数据的校验,增加业务逻辑
if (name.length() >= 2 && name.length() <= 6) {
this.name = name;
} else {
System.out.println("名字长度不合法,需要(2-6)个字符,给默认名字");
this.name = "无名小卒";
}
}
public int getAge() {
return age;
}
public void setAge(int age) {
// 判断
if (age >= 1 && age <= 120) {
this.age = age;
} else {
System.out.println("设置的年龄不合理,年龄需要在1-120");
this.age = 18; // 默认年龄
}
}
public double getSalary() {
// 增加对当前对象的权限判断
return salary;
}
public void setSalary(double salary) {
this.salary = salary;
}
public String getJob() {
return job;
}
public void setJob(String job) {
this.job = job;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", salary=" + salary +
", job='" + job + '\'' +
'}';
}
}
7.4、继承
继承(inherit)是面向对象的第二大特征。继承可以解决代码复用
的问题。当多个类存在相同的属性和方法时,可以从这些类中抽象出父类
。在父类中定义这些相同的属性和方法
,所有的子类不需要重新定义这些属性和方法,只需要通过extends
关键字来声明继承父类
即可。
继承的基本语法:
class 子类 extends 父类 {
}
继承父类后,子类就会自动拥有父类定义的属性和方法。
父类又叫 超类,基类,子类又叫派生类。
继承的作用:
- 提高代码的复用性。
- 提高代码的可扩展性和可维护性。
注意:
- 子类继承了父类所有的属性和方法,父类非私有的属性和方法可以在子类中直接访问。但是
父类的私有属性和方法不能在子类直接访问
,要通过父类提供的公共的方法去访问。 - 子类必须调用父类的构造器,先完成父类的初始化(先有父亲后有儿子)。
- 当创建子类对象时,不管使用子类的哪个构造器,默认情况下总会去调用父类的无参构造器
super();
。如果父类没有提供无参构造器,则必须在子类的构造器中用super
显式地指定使用父类的哪个构造器以完成对父类的初始化工作,否则编译不会通过。 - 如果希望指定去调用父类的某个个构造器,则显式的调用一下:
super(参数列表)
。 - 使用
super
时,需要放在子类构造器的第一行(和this在构造器中一样)且只能在构造器中使用。 super()
和this()
都只能放在构造器的第一行,因此这两个方法不能共存于一个构造器。- Java中所有的类都是
Object类
的子类 ->Object类是所有类的基类(超类)
。IDEA快捷键Ctrl + H
查看类的继承关系。 - 父类构造器的调用不限于直接父类。将一直向上追溯直到
Obejct类(顶级父类)
。 - 子类最多只能继承一个父类(指直接继承),即Java是单根继承机制。
- 不能滥用继承,子类和父类之间必须满足
is-a
的逻辑关系。
案例1:学生Student
(父类) <- 小学生Pupil
(子类),大学生Graduate
(子类)。双重继承,说明继承的作用。
Student.java
文件
package com.yxz.inherit_improve;
// 父类,是Pupil和Graduate的父类
public class Student {
// 共有属性
public String name;
public int age;
private double score;
// 共有方法
public void setScore(double score) {
this.score = score;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", score=" + score +
'}';
}
}
Pupil.java
文件
package com.yxz.inherit_improve;
// 子类Pupil继承父类Student
public class Pupil extends Student{
public void testing() {
System.out.println("小学生 " + name + " 正在考《小学数学》...");
}
}
Graduate.java
文件
package com.yxz.inherit_improve;
// 子类Graduate继承父类Student
public class Graduate extends Student{
public void testing() {
System.out.println("大学生 " + name + " 正在考《高等数学》...");
}
}
Inherit.java
文件——测试类
package com.yxz.inherit_improve;
public class Inherit {
public static void main(String[] args) {
// 小学生
Pupil pupil = new Pupil();
pupil.name = "小明";
pupil.age = 11;
pupil.testing();
pupil.setScore(60);
System.out.println(pupil.toString());
// 大学生
Graduate graduate = new Graduate();
graduate.name = "杨明";
graduate.age = 19;
graduate.testing();
graduate.setScore(98);
System.out.println(graduate.toString());
}
}
/**控制台输出:
* 小学生 小明 正在考《小学数学》...
* Student{name='小明', age=11, score=60.0}
* 大学生 杨明 正在考《高等数学》...
* Student{name='杨明', age=19, score=98.0}
*/
案例2:TopBase
<- Base
<- Sub
。三层继承,说明继承的十个注意点。
TopBase.java
文件
package com.yxz.inherit_improve;
public class TopBase {
public TopBase() {
// super(); Object类的无参构造器
System.out.println("TopBase()构造器被调用...");
}
}
Base.java
文件
package com.yxz.inherit_improve;
public class Base extends TopBase {// 父类
// 4个不同修饰符的属性
public int n1 = 100;
protected int n2 = 200;
int n3 = 300;
private int n4 = 400;
// public Base() {
// System.out.println("父类Base()构造器被调用...");
// }
public Base(String name, int age) {
// super(); TopBase类的无参构造器
System.out.println("父类Base(String name, int age)构造器被调用...");
}
// 父类提供一个public方法供子类访问父类私有属性和私有方法
public int getN4() {
return n4;
}
public void test100() {
System.out.println("test100");
}
protected void test200() {
System.out.println("test200");
}
void test300() {
System.out.println("test300");
}
private void test400() {
System.out.println("test400");
}
public void callTest400() {
test400();
}
}
Sub.java
文件
package com.yxz.inherit_improve;
public class Sub extends Base {
public Sub() {
// 子类必须调用父类的构造器,完成父类的初始化
// super(); // 默认调用父类的无参构造器 -> 子类中的无参构造器默认包含语句super();
// 当父类的无参构造器被覆盖时,必须在子类的构造器中显式的指定调用父类的其他有参构造器
super("小米", 10); // super()必须在子类的构造器的第一行(先有父亲再有儿子)
System.out.println("子类Sub()构造器被调用...");
}
public Sub(String name) {
super("小米", 10);
System.out.println("子类Sub(String name)构造器被调用...");
}
public void sayOk() {
// 父类的非private属性和方法都可以访问
System.out.println("n1 = " + n1 + " n2 = " + n2 + " n3 = " + n3);
//System.out.println(n4); 错误:不能直接访问父类的私有属性
test100();
test200();
test300();
//test400(); 错误:不能直接访问父类的私有方法
// 通过父类提供的公共的方法去访问父类的私有属性和私有方法
System.out.println("n4 = " + getN4());
callTest400();
}
}
InheritDetail.java
文件
package com.yxz.inherit_improve;
public class InheritDetail {
public static void main(String[] args) {
System.out.println("==========第1个对象===========");
Sub sub = new Sub();
sub.sayOk();
System.out.println("==========第2个对象===========");
Sub sub1 = new Sub("jack");
}
}
/**控制台输出:
* ==========第1个对象===========
* TopBase()构造器被调用...
* 父类Base(String name, int age)构造器被调用...
* 子类Sub()构造器被调用...
* n1 = 100 n2 = 200 n3 = 300
* test100
* test200
* test300
* n4 = 400
* test400
* ==========第2个对象===========
* TopBase()构造器被调用...
* 父类Base(String name, int age)构造器被调用...
* 子类Sub(String name)构造器被调用...
*
* 进程已结束,退出代码为 0
*/
继承的本质分析
(⭐⭐⭐)
子类查找(访问)属性和方法的规则
(1) 首先看子类是否有该属性(方法)。
(2) 如果子类有这个属性(方法)并且可以访问,则返回信息。
(3) 如果子类没有这个属性(方法),就看直接父类有没有这个属性(方法)。
如果直接父类有该属性(方法)且可以访问就返回父类的信息。
如果直接父类的该属性(方法)是私有的,且不可直接访问,则返回报错,不会继续向上访问父类。
(4) 如果直接父类没有就按照 (3) 的规则再向上找父类,直到Object类。
package com.yxz.inherit_improve;
public class InheritTheory {
public static void main(String[] args) {
Son son = new Son();
// 子类查找(访问)信息的规则:
// (1). 首先看子类是否有该属性
// (2). 如果子类有这个属性并且可以访问,则返回信息
// (3). 如果子类没有这个属性,就看直接父类有没有这个属性
// (直接父类有该属性且可以访问就返回信息,如果父类的该属性是私有的,则返回报错,不会继续向上访问)
// (4). 如果直接父类没有就按照(3)的规则再向上找父类,直到Object
System.out.println(son.name); // 大头儿子
System.out.println(son.age); // 39
System.out.println(son.hobby); // 旅游
}
}
class GrandPa {
String name = "大头爷爷";
String hobby = "旅游";
}
class Father extends GrandPa{
String name = "大头爸爸";
int age = 39;
}
class Son extends Father{
String name = "大头儿子";
}
7.5、多态
多态(polymorphic):方法或对象具有多种形态。多态是面向对象的第三大特征,多态是建立在封装和继承基础之上的。
多态的具体体现:
-
方法的多态:
方法重载和方法重写体现多态
。在同一方法名中传入不同参数时,会自动调用不同方法(重载)。当不同类对象调用同一方法名时,会自动调用不同方法(重写)。public class Test { public static void main(String[] args) { A a = new A(); B b = new B(); // 不同类对象调用同一方法名 -> 自动调用对应的方法(方法重写) a.say(); b.say(); // 传入不同个数的参数 -> 自动调用匹配的方法(方法重载) System.out.println(b.sum(1, 2)); System.out.println(b.sum(10, 20, 30)); } } class A {// 父类 public void say() { System.out.println("父类 A中的say()方法被调用"); } } class B extends A { public int sum(int n1, int n2) {// 和下面的sum方法构成方法重载 return n1 + n2; } public int sum(int n1, int n2, int n3) { return n1 + n2 + n3; } public void say() { // 和父类A中的say方法构成方法重写 System.out.println("子类 B中的say()方法被调用"); } }
-
对象的多态
(核心)编译类型 对象引用 = new 运行类型();
-
一个对象的编译类型和运行类型可以不一致。
-
编译类型在定义对象时就已经确定,不能改变。
-
对象的
运行类型是可以变化
的,可以通过getClass()方法
获取对象的运行类型。 -
编译类型看定义时
=
号的左边,运行类型看=
号的右边。Animal animal = new Animal(); // animal的编译类型和运行类型一致,都是Animal Animal animal = new Dog(); // animal编译类型是Animal,运行类型Dog animal = new Cat(); // animal的运行类型变成了Cat,编译类型仍然是Animal // 结论:父类的引用(对象名)可以指向父类对象,也可以指向父类的子类对象
package com.yxz.poly; public class PolyObject { public static void main(String[] args) { Animal animal = new Animal(); // animal 编译类型:Animal,运行类型:Animal animal.say(); animal = new Cat(); // animal 编译类型:Animal,运行类型:Cat animal.say(); animal = new Dog(); // animal 编译类型:Animal,运行类型:Dog animal.say(); } } /**控制台输出: * Animal cry() 动物叫... * Cat cry() 小猫喵喵叫... * Dog cry() 小狗汪汪叫... */ class Animal { public void say() { System.out.println("Animal cry() 动物叫..."); } } class Cat extends Animal { public void say() { System.out.println("Cat cry() 小猫喵喵叫..."); } } class Dog extends Animal { public void say() { System.out.println("Dog cry() 小狗汪汪叫..."); } }
-
注意:多态的前提是:两个对象(类)存在继承关系
多态的向上转型
-
向上转型的本质:父类的引用指向了子类的对象。
-
语法:
父类类型 对象引用 = new 子类类型();
-
向上转型调用方法的规则如下:
-
可以调用父类中的所有成员(遵守访问权限规则)。
-
不能调用子类中特有成员,无法通过编译。
原因:因为在编译阶段,能调用哪些成员,是由对象引用的编译类型来决定。
-
最终运行效果(运行阶段)看子类(对象引用的运行类型)的具体实现。即:在运行阶段调用方法时,根据对象引用的运行类型,从子类(运行类型)中开始查找方法(遵循访问成员的规则)。
-
多态的向下转型
- 语法:
子类类型 引用名 = (子类类型)父类引用
。 - 只能强转父类的引用,不能强转父类的对象。
- 要求父类的引用必须指向的是当前目标类型的对象。
- 当向下转型后,可以调用子类类型中的所有的成员(向下转型的目的)。
注意:
- 属性没有重写之说。属性的值看对象引用的编译类型。
instanceOf
比较操作符:用于判断对象的运行类型
是否为XX类型或XX类型的子类型。
package com.yxz.poly;
public class PolyDetail {
public static void main(String[] args) {
// 父类引用指向子类对象(向上转型)
// 语法:父类类型 引用名 = new 子类类型();
Animal animal = new Cat();
Object obj = new Cat();
// 可以调用父类中的所有成员(遵守访问权限)
// 不能调用子类的特有成员
// animal.catchMouse(); 编译错误,无法通过编译
// 在编译阶段,父类引用能调用哪些成员是由编译类型决定
// 最终运行效果(运行阶段)看子类(运行类型)的具体实现
// 子类重写了eat方法,所以优先调用子类的eat方法。
animal.eat(); // 猫吃鱼
animal.sleep(); // 睡觉
animal.run(); // 奔跑
animal.show();// 你好
// 属性的值看编译类型
System.out.println("animal.age = " + animal.age); // anmal编译类型:Animal,输出10
// animal的编译类型:Animal,运行类型:Cat
Cat cat = (Cat) animal; // 向下转型
cat.catchMouse();
System.out.println("cat.age = " + cat.age); // cat编译类型:Cat,输出20
System.out.println(cat instanceof Animal); // cat的运行类型:Cat,是Animal或其子类:true
System.out.println(animal instanceof Cat); // animal的运行类型:Cat,是Cat或其子类:true
Object obj1 = new Object();
System.out.println(obj1 instanceof Animal); // obj1的运行类型:Object,是Animal或其子类:false
}
}
/**
* 控制台输出:
* 猫吃鱼
* 睡觉
* 奔跑
* 你好
* animal.age = 10
* 猫抓老鼠
* cat.age = 20
* true
* true
* false
*/
class Animal {
String name = "动物";
int age = 10;
public void sleep() {
System.out.println("睡觉");
}
public void run() {
System.out.println("奔跑");
}
public void eat() {
System.out.println("吃饭");
}
public void show() {
System.out.println("你好");
}
}
class Cat extends Animal {
int age = 20;
public void eat() {
System.out.println("猫吃鱼");
}
public void catchMouse() {// Cat特有方法
System.out.println("猫抓老鼠");
}
}
Java的动态绑定机制(非常非常非常重要)
- 当调用对象方法的时候,该方法会和该对象的内存地址(运行类型)进行绑定。
- 当调用对象属性时,没有动态绑定机制,哪里声明,哪里使用。
package com.yxz.poly;
public class DynamicBinding {
public static void main(String[] args) {
A a = new B();// 向上转型
// 1. a调用方法sum(),先从运行类型(B类)开始查找
// 2. 运行类型中没有sum方法 —> 根据继承机制,到父类中查找sum方法
// 3. 父类中有sum方法,调用
// 4. 父类sum方法中调用了getI方法(子类父类都有),根据动态绑定机制 —> 该方法和该对象内存地址(运行类型)绑定
// 5. 再到运行类型(B类)中查找getI方法,在B类中找到getI方法
// 6. getI方法访问属性i,根据属性没有动态绑定机制,哪里声明哪里使用,先找子类中的i = 20
// 7. 返回到父类中的sum方法,返回i + 10 = 30
System.out.println(a.sum()); // 30
System.out.println(a.sum1()); // 20
}
}
class A {
public int i = 10;
public int sum() {
return getI() + 10;
}
public int sum1() {
return i + 10;
}
public int getI() {// 父类getI
return i;
}
}
class B extends A {
public int i = 20;
// public int sum() {
// return getI() + 20;
// }
// public int sum1() {
// return i + 10;
// }
public int getI() {// 子类getI
return i;
}
}
多态的应用
-
多态数组
数组的定义类型为父类类型,里面保存的实际元素类型为子类类型。
应用实例:创建一个Person对象、2个Student对象和2个Teacher对象,统一放在数组中,并调用每个对象的say方法。
Person.java
文件package com.yxz.poly.polyarr; 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 void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String say() { return name + "\t" + age; } }
Student.java
文件package com.yxz.poly.polyarr; public class Student extends Person { private double score; public Student(String name, int age, double score) { super(name, age); this.score = score; } public double getScore() { return score; } public void setScore(double score) { this.score = score; } @Override public String say() { return "学生 " + super.say() + " \t\tscore = " + score; } // 特有方法 public void study() { System.out.println("学生" + getName() + "正在学java..."); } }
Teacher.java
文件package com.yxz.poly.polyarr; public class Teacher extends Person { private double salary; public Teacher(String name, int age, double salary) { super(name, age); this.salary = salary; } public double getSalary() { return salary; } public void setSalary(double salary) { this.salary = salary; } @Override public String say() { return "老师 " + super.say() + " \t\tsalary = " + salary; } // 特有方法 public void teach() { System.out.println("老师" + getName() + "正在授课..."); } }
PloyArray.java
文件package com.yxz.poly.polyarr; public class PloyArray { public static void main(String[] args) { Person[] persons = new Person[5]; persons[0] = new Person("jack", 20); persons[1] = new Student("mary", 18, 100); persons[2] = new Student("smith", 19, 30.1); persons[3] = new Teacher("milan", 30, 20000); persons[4] = new Teacher("king", 29, 25000); // 循环遍历多态数组,调用say for (int i = 0; i < persons.length; i++) { // persons[i] 编译类型:Person,运行类型是根据实际情况由JVM判断 System.out.println(persons[i].say()); // 动态绑定机制 if (persons[i] instanceof Student) { // 判断运行类型是否是Student类 ((Student) persons[i]).study(); //向下转型 } else if (persons[i] instanceof Teacher) { // 判断运行类型是否是Teacher类 ((Teacher) persons[i]).teach(); //向下转型 } else if (persons[i] instanceof Person) { // 判断运行类型是否是Person类 // 不做任何处理 } else { System.out.println("类型有误..."); } } } }
-
多态参数
方法定义的形参类型为父类类型,实参类型允许为子类类型。
案例1:主人喂动物食物。
- 定义一个动物类Animal,属性name(private)。狗类Dog和猫类Cat继承动物类。
- 定义一个食物类Food,属性name(private)。骨头类Bone和鱼类Fish继承食物类。
- 定义一个主人类Master,属性name(private)。添加一个喂食feed(Animal animal, Food food)方法,根据传入的不同类型分别打印不同的信息。
- 测试类中调用方法feed,分别传入不同对象。
Animal.java
文件package com.yxz.poly.method; public class Animal {// 动物类 private String name; public Animal(String name) { this.name = name; } public String getName() { return name; } public void setName(String name) { this.name = name; } } class Dog extends Animal { public Dog(String name) { super(name); } } class Cat extends Animal { public Cat(String name) { super(name); } }
Food.java
文件package com.yxz.poly.method; public class Food {// 食物类 private String name; public Food(String name) { this.name = name; } public String getName() { return name; } public void setName(String name) { this.name = name; } } class Fish extends Food { public Fish(String name) { super(name); } } class Bone extends Food { public Bone(String name) { super(name); } }
Master.java
文件package com.yxz.poly.method; public class Master {// 主人类 private String name; public Master(String name) { this.name = name; } public String getName() { return name; } public void setName(String name) { this.name = name; } public void feed(Animal animal, Food food) { // 参数多态 System.out.println("主人" + getName() + "给宠物 " + animal.getName() + " 喂 " + food.getName() + " 吃"); } }
Polymorphic.java
——测试类package com.yxz.poly.method; public class Polymorphic { public static void main(String[] args) { // 创建主人对象 Master tom = new Master("tom"); // 创建小狗 Dog dog = new Dog("大黄"); Bone bone = new Bone("大棒骨"); tom.feed(dog, bone); // 创建小猫 Cat cat = new Cat("花猫"); Fish fish = new Fish("黄花鱼"); tom.feed(cat, fish); } }
案例2:计算不同员工的年薪
- 定义员工类Employee,属性包含姓名和月工资(private),以及计算年工资getAnnual方法。
- 普通员工Worker和经理Manager继承了员工。经理类多了奖金bonus属性和管理manage方法,普通员工类多了work方法,普通员工和经理类要求分别重写getAnnual方法。
- 测试类中添加一个方法showEmployeeAnnual(Employee e),实现获取任何员工对象的年工资,并在main方法中调用该方法e.getAnnual。
- 测试类中添加一个方法testWork(Employee e),如果是普通员工,则调用work方法,如果是经理,则调用manage方法。
Employee.java
文件package com.yxz.ployparameter; public class Employee { private String name; private double salary; public Employee(String name, double salary) { this.name = name; this.salary = salary; } /** * 获取年工资的方法 * * @return :年工资 */ public double getAnnual() { return 12 * salary; } public String getName() { return name; } public void setName(String name) { this.name = name; } public double getSalary() { return salary; } public void setSalary(double salary) { this.salary = salary; } }
Worker.java
文件package com.yxz.ployparameter; public class Worker extends Employee { public Worker(String name, double salary) { super(name, salary); } @Override public double getAnnual() { return super.getAnnual(); } public void work() { System.out.println("普通员工 " + getName() + " is working..."); } }
Manager.java
文件package com.yxz.ployparameter; public class Manager extends Employee { private double bonus; public Manager(String name, double salary, double bonus) { super(name, salary); this.bonus = bonus; } public double getBonus() { return bonus; } public void setBonus(double bonus) { this.bonus = bonus; } @Override public double getAnnual() { return super.getAnnual() + bonus; } public void manage() { System.out.println("经理" + getName() + " is managing..."); } }
PloyParameter.java
——测试类package com.yxz.ployparameter; public class PloyParameter { public static void main(String[] args) { Worker tom = new Worker("Tom", 3000); Manager miCai = new Manager("MiCai", 13000, 200000); PloyParameter ployParameter = new PloyParameter(); ployParameter.showEmployeeAnnual(tom); ployParameter.testWork(tom); ployParameter.showEmployeeAnnual(miCai); ployParameter.testWork(miCai); } public void showEmployeeAnnual(Employee e) { System.out.println(e.getAnnual()); // 动态绑定机制 } // testWork方法:如果是普通员工,调用work方法,如果是经理,调用manage方法 public void testWork(Employee e) { if (e instanceof Worker) { ((Worker) e).work(); // 向下转型 } else if (e instanceof Manager) { ((Manager) e).manage(); // 向下转型 } else { // 不做处理 } } }
7.6、super
super
代表父类的引用,用于访问父类的属性、方法、构造器。
基本语法:
super.属性名;
访问父类的属性,但不能访问父类的private属性。super.方法名(参数列表);
访问父类的方法,但不能访问父类的private方法。super(参数列表);
访问父类的构造器,只能在构造器的第一个语句。
super的作用:
-
调用父类的构造器 -> 分工明确,父类属性由父类初始化,子类的属性由子类初始化。
-
当子类中有和父类中的成员(属性和方法)重名时,为了访问父类的成员,必须通过
super
。如果没有重名,使用super、this、直接访问是一样的效果。使用
直接访问
、this
访问属性遵从就近原则
,先在本类中找属性或方法,找不到再找父类。super
会直接跳过本类直接到父类
中找属性或方法。 -
super
的访问不限于直接父类,如果爷爷类和本类中有同名的成员,也可以用super
去访问爷爷类的成员。如果多个基类(超类)都有同名成员,使用super
访问遵循就近原则
super和this的比较
区别点 | this |
super |
---|---|---|
访问属性 | 优先访问本类中的属性 ,如果本类中没有此属性则从父类中继续查找 |
直接从父类开始 访问属性 |
调用方法 | 优先访问本类中的方法 ,如果本类中没有此方法则从父类中继续查找 |
直接从父类开始 访问方法 |
调用构造器 | 调用本类构造器,必须放在构造器的首行 | 调用父类构造器,必须放在子类构造器的首行 |
特殊 | 表示当前对象 | 子类中访问父类对象 |
7.7、overwrite
方法覆盖(方法重写, overwrite, override):子类有一个方法和父类的某个方法的名称、返回类型、参数一样
,那么子类的这个方法就覆盖了父类的方法。不局限于直接父类,间接父类也可以触发方法覆盖。
构成方法覆盖的条件:
- 子类的方法的方法名、参数列表要和父类的方法的方法名、参数列表完全一样才能构成方法覆盖。
- 子类方法的返回值类型和父类方法的
返回值类型一样,或者是父类返回值类型的子类
。如:父类方法返回值类型Object类,子类方法返回值类型是String,其他一样,则构成方法覆盖。 - 子类方法不能缩小(可以放大或相等)父类方法的访问权限。
public > protected > 默认 > private
public class A {
public void hi() {
}
protected Object say() {
return null;
}
}
class B extends A {
public void hi() {// 构成方法覆盖
}
public void hi(String name) {// 构成方法重载,不构成方法覆盖(形参列表不一致)
}
private void hi(int age) {// 构成方法重载,不构成方法覆盖(缩小了父类方法的访问范围)
}
public Object say() {// 构成方法覆盖(扩大了父类方法访问范围)
return null;
}
// protected String say() {// 不构成方法重载,可以构成方法覆盖(返回类型String是父类方法返回类型Object类的子类)
// return null;
// }
}
方法重载(overload)和方法重写(override)的区别
名称 | 发生范围 | 方法名 | 形参列表 | 返回值类型 | 访问修饰符 |
---|---|---|---|---|---|
重载(overload) | 本类 | 必须一致 | 参数类型、参数个数、参数顺序至少一个不同 | 无要求 | 无要求 |
重写(override) | 父子类 | 必须一致 | 必须一致 | 子类重写的方法,返回值类型和父类返回值类型一致或者是其子类 | 子类方法的访问修饰符不能缩小父类方法的访问范围 |
案例(综合练习)
- 编写一个Person类,包括
name, age
属性(private),构造器、方法say(返回自我介绍的字符串)。 - 编写一个Student类,继承Person类,增加
id、socre
属性(private),以及构造器,定义say方法(返回自我介绍的字符串)。 - 在main()方法中,分别创建Person和Student对象,调用say方法输出自我介绍。
public class Test {
public static void main(String[] args) {
Person person = new Person("jack", 34);
System.out.println(person.say());
Student student = new Student("杨明", 21, 123, 98.5);
System.out.println(student.say());
}
}
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 void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String say() {
return "name = " + name + " age = " + age;
}
}
class Student extends Person {
private int id;
private double score;
public Student(String name, int age, int id, double score) {
super(name, age);
this.id = id;
this.score = score;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public double getScore() {
return score;
}
public void setScore(double score) {
this.score = score;
}
public String say() {
return super.say() + " id = " + id + " score = " + score;
}
}
7.8、Object类详解
-
equals()方法
==和equals的对比
-
==:既可以判断基本类型,又可以判断引用类型。
如果判断基本类型,判断的是值是否相等。如:
int i = 10; double d = 10.0;
如果判断引用类型,判断的是对象地址是否相等,即判定是不是同一个对象。
-
equals():是Object类中的方法,只能判断引用类型。
默认判断的是地址是否相等
,子类中往往重写该方法,用于判断内容是否相等。如:Integer类和String类的equals()方法已经重写,不再判断地址,只判断内容。
Object类的equals()方法源码:
public boolean equals(Object obj) { return (this == obj); // 判断对象地址是否相等 }
查看JDK中
Integer的equals()方法源码
和String类的equals()方法源码
:// Integer类的equals()方法 public boolean equals(Object obj) {// 重写Object类的equals()方法 if (obj instanceof Integer) { // 判断对象是否属于Integer类 return value == ((Integer)obj).intValue(); // 向下转型并调用Integer的intValue()方法 } return false; }
// String类的equals()方法 public boolean equals(Object anObject) {// 重写Object类的equals()方法 if (this == anObject) { // 如果是同一个对象则返回真 return true; } if (anObject instanceof String) { // 判断对象的运行类型是否属于String类 String aString = (String)anObject; // 向下转型,子类类型 对象引用 = (子类类型)父类引用 if (!COMPACT_STRINGS || this.coder == aString.coder) { return StringLatin1.equals(value, aString.value); } } return false; // 如果对象不属于String类,返回假 }
-
实例:创建一个Person类,属性包括name、age、gender(private),
判断两个Person对象的内容是否相等,如果两个Person对象的各个属性都一样,则返回true,反之false。
package com.yxz.object_; public class EqualsExercise01 { public static void main(String[] args) { Person person1 = new Person("jack", 10, '男'); Person person2 = new Person("jack", 10, '男'); System.out.println(person1.equals(person2)); // true System.out.println(person1 == person2); // false ==对引用类型判断是否是同一个对象地址 } } class Person { private String name;// 姓名 private int age;// 年龄 private char gender;// 性别 public Person(String name, int age, char gender) { this.name = name; this.age = age; this.gender = gender; } @Override public boolean equals(Object obj) { // 如果是同一个对象,返回true if (this == obj) { return true; } // 如果obj是Person对象 if (obj instanceof Person) { // 将obj对象向下转型,目的:需要得到子类的所有属性和方法 Person p = (Person) obj; return this.name.equals(p.name) && this.age == p.age && this.gender == p.gender; } // 如果不是Person对象 return false; } }
-
-
finalize()方法
说明:- 当对象被回收时,系统自动调用该对象的finalize()方法。子类可以重写该方法,做一些释放资源的操作。
- 什么时候被回收:当
某个对象没有任何引用
时,则JVM就认为这个对象是一个垃圾对象
,就会使用垃圾回收机制
来销毁该对象,在销毁该对象前,会先调用finalize()方法
。 - 垃圾回收机制的调用,是由系统决定,也可以通过
System.gc()
主动触发垃圾回收机制。
public class Finalize{ public static void main(String[] args) { Car bmw = new Car("宝马"); bmw = null; // 此时bmw指向的对象就是一个垃圾,垃圾回收器就会根据GC算法回收(销毁)对象,在销毁对象前,会调用该对象finalize()方法 } } class Car { private String name; public Car(String name) { this.name = name; } }
-
hashCode()方法
返回该对象的哈希码值(一般是通过将该对象的内部地址转换成一个整数来实现的),Object类支持此方法是为了提高哈希表的性能。
结论:
- 提高具有哈希结构的容器的效率。
- 两个引用,如果指向的是同一个对象,则哈希值肯定是一样的。
- 两个引用,如果指向的是不同对象,则哈希值肯定不一样。
- 哈希值主要根据地址号来的,不能完全将哈希值等价于地址。
- 在集合等容器中如果需要hashCode,也会重写此方法。
-
toString()方法
该方法默认返回:全类名+@+哈希值的十六进制。(全类名 = 包名 + 类名) 子类往往重写toString方法,用于返回对象的属性信息。在IDEA中使用快捷键
Alt + Insert
可以选择根据具体实现类的属性重写Object的toString()方法。重写toString()方法,打印对象或者拼接对象时,都会自动调用该对象的toString形式。
当直接输出一个对象时,toString()方法会被默认调用。
Object类的toString()方法源码
:public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode()); }
说明:
- getClass():返回class+包名+类名. 如:class java.lang.String
- getClass().getName():获取全类名(包名+类名)。如:java.lang.String
- getClass().getSimpleName():获取类名. 如:String
Integer.toHexString(hashCode())
:将对象的hashCode值转成十六进制字符串。
案例:定义一个Monster类,属性包括name、job、salary。将Monster对象的属性输出
package com.yxz.object_; public class ToStringExercise { public static void main(String[] args) { Monster monster = new Monster("小妖怪", "巡山的", 1000); System.out.println(monster.toString()); System.out.println(monster); // 默认调用toString()方法 } } class Monster { private String name; private String job; private double salary; public Monster(String name, String job, double salary) { this.name = name; this.job = job; this.salary = salary; } @Override public String toString() { return "Monster{" + "name='" + name + '\'' + ", job='" + job + '\'' + ", salary=" + salary + '}'; } }
7.9、断点调试
在开发中,程序员查找错误时,可以用断点调试(breakpoint debug),一步一步的看源码执行的过程,从而发现错误所在。
重要提示:在断点调试过程中,是运行状态,是以对象的运行类型来执行的。
介绍:
- 断点调试是指在程序的某一行设置一个断点,调试时,程序运行到这一行就会停住,然后可以一步一步往下跳是,调试过程中可以看各个变量当前的值。出错的话,调试到出错的代码行即显示错误,停下。进行分析找到bug。
- 断点调试是程序员必须掌握的技能。
- 断点调试也能帮助我们查看Java底层源码的执行过程,提高Java水平。
- 断点调试快捷键:
F7
:跳入 -> 跳入方法体内执行。F8
:跳过 -> 逐行执行代码。Shift + F8
:跳出 -> 跳出方法。F9
:resume,执行到下一个断点。
8、项目实战运用
8.1、零钱通
项目需求:
使用Java开发零钱通项目,可以完成查看明细(功能1),收益入账(功能2),消费(功能3),退出系统(功能4)等功能。
在用户输入4时,给出提示“确定要退出吗?y/n”,必须输入正确的y/n,否则循环输入指令,直到输入y/n。
在收益入账和消费时,判断金额是否合理,并给出相应提示。
项目流程(化繁为简)
-
先完成菜单显示功能,并可以选择功能。
-
完成零钱通明细功能。方法:1、把收益入账和消费保存到数组 2、使用对象 3、使用String拼接。
-
完成收益入账功能。
-
完成消费功能。
-
退出系统功能。
-
在用户输入4时,给出提示“确定要退出吗?y/n”,必须输入正确的y/n,否则循环输入指令,直到输入y/n。
思路分析:
(1)定义一个变量choice,接收用户输入
(2)使用while + break,来处理接收到的输入是 y 还是 n
(3)退出while后,再判断choice是y还是n,就可以决定是否退出
(4)建议一段代码,完成一个小功能,降低耦合度。 -
在收益入账和消费时,判断金额是否合理,并给出相应提示。
如何校验数据?过关斩将思想:排除所有非法数据即可 -> 先判断是否是非法数据,再执行正确数据的代码(提高代码可读性)
-
使用面向过程完成项目的功能后,使用OOP(Object-oriented Programming)思想封装(变量 -> 类属性,功能 -> 类方法)
项目源码
SmallChangeSysOOP.java
文件
package com.yxz.smallchange.oop;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Scanner;
/**
* 该类是完成零钱通的各个功能的类
* 使用OOP(Object-Oriented Programming),面向对象编程
* 各个功能对应一个方法
*/
public class SmallChangeSysOOP {
// 类属性
// 1. 完成菜单显示功能,定义相关变量
boolean loop = true;
Scanner scanner = new Scanner(System.in);
String key = "";
// 2. 完成零钱通明细
String details = "------------------零钱通明细--------------";
// 3. 完成收益入账
double money = 0; // 入账金额
double balance = 0; // 余额
Date date = null; // date 是 java.util.Date类型,表示日期
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm"); // 用于日期格式化的对象
// 4. 完成消费
// 定义新变量,记录消费原因
String note = "";
public void mainMenu() {
do {
System.out.println("\n==================零钱通菜单==============");
System.out.println("\t\t\t\t1 零钱通明细");
System.out.println("\t\t\t\t2 收益 入账");
System.out.println("\t\t\t\t3 消费 支出");
System.out.println("\t\t\t\t4 退出 系统");
System.out.print("请选择(1-4): ");
key = scanner.next();
switch (key) {
case "1":
this.detail();
break;
case "2":
this.income();
break;
case "3":
this.pay();
break;
case "4":
this.exit();
break;
default:
System.out.println("选择有误,请重新选择...");
}
} while (loop);
System.out.println("---------------已经退出零钱通--------------");
}
// 完成零钱通明细
public void detail() {
System.out.println(details);
}
// 收益入账
public void income() {
System.out.print("收益入账金额:");
money = scanner.nextDouble();
// money的范围需要校验
if (money <= 0) {
System.out.println("收益入账金额需要大于0元");
return; // 退出方法
}
balance += money;
// 拼接收益入账信息到 details
date = new Date(); // 获取到当前日期
System.out.println();
details += "\n收益入账\t+" + money + "\t" + sdf.format(date) + "\t" + "余额:" + balance;
}
// 消费
public void pay() {
System.out.print("消费金额:");
money = scanner.nextDouble();
// money的范围需要校验
if (money <= 0 || money > balance) {
System.out.println("消费金额应该在0 - " + balance + "元");
return;
}
System.out.print("消费说明:");
note = scanner.next();
balance -= money; // 余额变化
// 拼接消费信息到 details
date = new Date(); // 获取到当前日期
System.out.println();
details += "\n" + note + "\t-" + money + "\t" + sdf.format(date) + "\t" + "余额:" + balance;
}
public void exit() {
/*
思路分析:
(1)定义一个变量choice,接收用户输入
(2)使用while + break,来处理接收到的输入是 y 还是 n
(3)退出while后,再判断choice是y还是n,就可以决定是否退出
(4)建议一段代码,完成一个小功能,降低耦合度。
*/
String choice = "";
while (true) { // 要求用户必须输入y/n,否则一直循环
System.out.print("你确定要退出吗?y/n: ");
choice = scanner.next();
if ("y".equals(choice) || "n".equals(choice)) {
break;
}
}
// 当用户退出while后,进行判断
if (choice.equals("y")) {
loop = false;
}
}
}
SmallChangeSysApp.java
文件
package com.yxz.smallchange.oop;
/**
* 测试类,调用SmallChangeSysOOP对象方法
*/
public class SmallChangeSysApp {
public static void main(String[] args) {
new SmallChangeSysOOP().mainMenu(); // 匿名对象,用了一次就销毁
}
}
8.2、房屋出租系统
房屋租赁程序框架图(分层模式) ——> 系统有哪些类(文件),明确类与类的调用关系
每个层次完成不同功能 -> 各司其职
-
HouseVies.java类
[View层,界面层]- 显示界面
- 接收用户的收入
- 调用HouseService完成对房屋信息的各种操作
代码实现分析:
- 编写mainMenu()方法,界面显示主菜单。
- 编写listHouses()方法,调用业务层的list()方法,界面显示房屋列表信息。
- 编写addHouse()方法,界面接收用户输入,创建House对象,调用业务层add()方法。
- 编写delHouse()方法,界面接收用户输入的房屋id,调用业务层的del()方法。
- 编写updateHouse()方法,界面接收用户输入的房屋id,调用业务层的find()方法查找对象并修改。
- 编写findHouse()方法,界面接收用户输入的房屋id,调用业务层的find()方法。
- 编写exit()方法,界面接收用户输入的选择。
源码:
package com.yxz.house.view; import com.yxz.house.domain.House; import com.yxz.house.service.HouseService; import com.yxz.house.utils.Utility; /** * 1. View层,显示界面 * 2. 接收用户的输入 * 3. 调用HouseService完成对房屋信息的各种操作(CRUD) */ public class HouseView { // 属性 private boolean loop = true; // 控制显示菜单 private final HouseService houseService = new HouseService(2); // 声明一个HouseService对象,用于listHouses方法调用其list()方法 /** * 显示主菜单的方法 */ public void mainMenu() { do { System.out.println("===================房屋出租系统菜单==================="); System.out.println("\t\t\t\t 1. 新 增 房 源"); System.out.println("\t\t\t\t 2. 查 找 房 屋"); System.out.println("\t\t\t\t 3. 删 除 房 屋 信 息"); System.out.println("\t\t\t\t 4. 修 改 房 屋 信 息"); System.out.println("\t\t\t\t 5. 房 屋 列 表"); System.out.println("\t\t\t\t 6. 退 出 系 统"); System.out.print("请输入你的选择(1-6):"); // 接收用户选择 char key = Utility.readChar(); switch (key) { case '1' -> addHouse(); case '2' -> findHouse(); case '3' -> delHouse(); case '4' -> updateHouse(); case '5' -> listHouses(); case '6' -> exit(); default -> System.out.println("没有此功能,请重新选择...\n"); } } while (loop); } /** * 调用业务层的list()方法,界面显示房屋列表信息 */ public void listHouses() { System.out.println("-------------------房 屋 列 表 信 息------------------"); System.out.println("编号\t房主\t电话\t\t\t地址\t\t月租\t状态(未出租/已出租)"); House[] houses = houseService.list(); // 得到所有房屋信息 for (House house : houses) { if (house == null) { break; } System.out.println(house); } System.out.println("-------------------房屋列表显示完毕-------------------\n"); } /** * 界面接收用户输入,创建House对象,调用业务层add()方法 */ public void addHouse() { System.out.println("-------------------添 加 房 屋 信 息-------------------"); System.out.print("姓名: "); String name = Utility.readString(8); System.out.print("电话: "); String phone = Utility.readString(11); System.out.print("地址: "); String address = Utility.readString(16); System.out.print("月租: "); int rent = Utility.readInt(); System.out.print("状态: "); String state = Utility.readString(3); // 创建一个新的House对象, 注意id是系统分配的, House newHouse = new House(0, name, phone, address, rent, state); if (houseService.add(newHouse)) { System.out.println("-------------------添 加 房 屋 成 功-------------------\n"); } else { System.out.println("-------------------添 加 房 屋 失 败-------------------\n"); } } /** * 界面接收用户输入的房屋id,调用业务层的del()方法 */ public void delHouse() { System.out.println("-------------------删 除 房 屋 信 息------------------"); System.out.print("请输入待删除房屋的编号id(-1退出):"); int delId = Utility.readInt(); if (delId == -1) { System.out.println("-------------------放弃删除房屋信息-------------------\n"); return; } char choice = Utility.readConfirmSelection(); // 该方法有循环判断是否选择y/n的逻辑 if (choice == 'Y') { if (houseService.del(delId)) { System.out.println("------------------删 除 房 屋 成 功-------------------\n"); } else { System.out.println("--------------------房屋信息不存在--------------------\n"); } } else { System.out.println("-------------------放弃删除房屋信息-------------------\n"); } } /** * 根据id查找房屋信息,界面接受用户输入的房屋id,调用业务层find()方法 */ public void findHouse() { System.out.println("-------------------查 找 房 屋 信 息------------------"); System.out.print("请输入需要查找的房屋id:"); int findId = Utility.readInt(); // 调用find方法 House house = houseService.find(findId); if (house != null) { System.out.println(house); } else { System.out.println("--------------------房屋信息不存在--------------------\n"); } } /** * 修改房屋信息,界面接收要修改的房屋id,调用业务层的find()方法返回要修改的房屋对象并其修改属性 */ public void updateHouse() { System.out.println("-------------------修 改 房 屋 信 息------------------"); System.out.print("请选择待修改房屋编号(-1退出):"); int updateId = Utility.readInt(); if (updateId == -1) { System.out.println("-------------------放弃修改房屋信息-------------------\n"); return; } House house = houseService.find(updateId); if (house == null) { System.out.println("--------------------房屋信息不存在--------------------\n"); return; } System.out.print("姓名(" + house.getName() + "):"); String name = Utility.readString(8, ""); if (!"".equals(name)) { house.setName(name); } System.out.print("电话(" + house.getPhone() + "):"); String phone = Utility.readString(11, ""); if (!"".equals(phone)) { house.setPhone(phone); } System.out.print("地址(" + house.getAddress() + "):"); String address = Utility.readString(16, ""); if (!"".equals(address)) { house.setAddress(address); } System.out.print("租金(" + house.getRent() + "):"); int rent = Utility.readInt(-1); if (rent != -1) { house.setRent(rent); } System.out.print("状态(" + house.getState() + "):"); String state = Utility.readString(3, ""); if (!"".equals(state)) { house.setState(state); } System.out.println("-------------------修改房屋信息成功-------------------\n"); } /** * 退出房屋出租系统 */ public void exit() { // 使用工具类提供的方法 char c = Utility.readConfirmSelection(); if (c == 'Y') { loop = false; System.out.println("=================已退出房屋出租系统==================="); } } }
-
HouseService.java类
[Service层,业务层]定义House[],保存House对象。
- 响应HouseView的调用。
- 完成对房屋信息的各种操作(增删改查,C-Creat R-Read U-Update D-Delete)
代码实现分析:
- 编写list()方法,返回所有的房屋信息。
- 编写add(House newHouse)方法,把newHouse对象加入到houses数组,并返回bool值。
- 编写del(int delId)方法,完成真正的删除任务,返回bool值表示删除操作是否成功。
- 编写find(int findId)方法,查找指定id的房屋对象,如果有则返回对象,没有则返回null。
源码:
package com.yxz.house.service; import com.yxz.house.domain.House; /** * HouseService 业务层 * 定义House[],保存House对象。 * 1. 响应HouseView的调用。 * 2. 完成对房屋信息的各种操作(增删改查,C-Creat R-Read U-Update D-Delete) */ public class HouseService { private House[] houses; private int houseNums = 1; // 记录当前有多少房屋信息 private int idCounter = 1; // 记录当前id增长到哪个值 public HouseService(int size) { houses = new House[size]; // 当创建一个HouseService对象,指定数组大小 houses[0] = new House(1, "jack", "189xxxxxxxx", "上海市", 15000, "未出租"); } /** * list方法,返回所有房屋信息 * * @return :返回House[] */ public House[] list() { return houses; } /** * 添加新的房屋对象 * * @param newHouse :新增加的房屋信息 * @return :表示增加的操作是否成功 */ public boolean add(House newHouse) { // 判断是否可以继续添加 if (houseNums == houses.length) { // 数组已满,不能再添加 // 数组扩容 House[] tempHouses = new House[houses.length + 1]; // 创建一个临时数组,存放原houses数组的对象 System.arraycopy(houses, 0, tempHouses, 0, houses.length); houses = tempHouses; // 修改原数组引用houses指向的对象地址 } // 把newHouse对象加入到houses[] houses[houseNums++] = newHouse; // 程序员需要设计一个id自增长的机制,更新newHouse的id newHouse.setId(++idCounter); return true; } /** * 删除指定房屋id的房屋信息 * * @param delId :要删除房屋信息的房屋id * @return :表示房屋删除操作是否成功。 */ public boolean del(int delId) { // 先查询到要删除的房屋信息对应的下标,下标与房屋id不一致 int index = -1; for (int i = 0; i < houseNums; i++) { if (delId == houses[i].getId()) { // 要删除的房屋(id),是数组下标为i的元素 index = i; } } if (index == -1) { // 说明delId在数组中不存在 return false; } for (int i = index; i < houseNums - 1; i++) {// 从被删除的id开始,将后面的房屋信息前移 houses[i] = houses[i + 1]; } houses[--houseNums] = null; // 将此时的房屋数-1,再把当前最后一个房屋置空 return true; } /** * 根据用户输入的id,查找房屋信息 * * @param findId :房屋id * @return :表示查找到的房屋对象 */ public House find(int findId) { for (int i = 0; i < houseNums; i++) { if (findId == houses[i].getId()) { return houses[i]; } } return null; } }
-
House.java类
[Model层,domain/数据层]- 一个House对象表示一个房屋信息。
源码:
package com.yxz.house.domain; /** * House类的对象表示一个房屋信息 */ public class House { // 属性:编号、房主、电话、地址、月租、状态(未出租/已出租) private int id; private String name; private String phone; private String address; private int rent; private String state; // 构造器 和 setter/getter方法 public House(int id, String name, String phone, String address, int rent, String state) { this.id = id; this.name = name; this.phone = phone; this.address = address; this.rent = rent; this.state = state; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public int getRent() { return rent; } public void setRent(int rent) { this.rent = rent; } public String getState() { return state; } public void setState(String state) { this.state = state; } @Override public String toString() { return id + "\t\t" + name + "\t" + phone + "\t\t" + address + "\t\t" + rent + "\t" + state; } }
-
Utility.java类
[Utility工具类]在实际开发中,公司都会提供相应的工具类和开发库,可以提高开发效率。
-
了解Utility类的使用
Utility.java
源码/** * 工具类的作用: * 处理各种情况的用户输入,并且能够按照程序员的需求,得到用户的控制台输入。 */ import java.util.*; public class Utility { //静态属性 private static Scanner scanner = new Scanner(System.in); /** * 功能:读取键盘输入的一个菜单选项,值:1——5的范围 * * @return 1——5 */ public static char readMenuSelection() { char c; for (; ; ) { String str = readKeyBoard(1, false);//包含一个字符的字符串 c = str.charAt(0);//将字符串转换成字符char类型 if (c != '1' && c != '2' && c != '3' && c != '4' && c != '5') { System.out.print("选择错误,请重新输入:"); } else break; } return c; } /** * 功能:读取键盘输入的一个字符 * * @return 一个字符 */ public static char readChar() { String str = readKeyBoard(1, false);//就是一个字符 return str.charAt(0); } /** * 功能:读取键盘输入的一个字符,如果直接按回车,则返回指定的默认值;否则返回输入的那个字符 * * @param defaultValue 指定的默认值 * @return 默认值或输入的字符 */ public static char readChar(char defaultValue) { String str = readKeyBoard(1, true);//要么是空字符串,要么是一个字符 return (str.length() == 0) ? defaultValue : str.charAt(0); } /** * 功能:读取键盘输入的整型,长度小于2位 * * @return 整数 */ public static int readInt() { int n; for (; ; ) { String str = readKeyBoard(10, false);//一个整数,长度<=10位 try { n = Integer.parseInt(str);//将字符串转换成整数 break; } catch (NumberFormatException e) { System.out.print("数字输入错误,请重新输入:"); } } return n; } /** * 功能:读取键盘输入的 整数或默认值,如果直接回车,则返回默认值,否则返回输入的整数 * * @param defaultValue 指定的默认值 * @return 整数或默认值 */ public static int readInt(int defaultValue) { int n; for (; ; ) { String str = readKeyBoard(10, true); if (str.equals("")) { return defaultValue; } //异常处理... try { n = Integer.parseInt(str); break; } catch (NumberFormatException e) { System.out.print("数字输入错误,请重新输入:"); } } return n; } /** * 功能:读取键盘输入的指定长度的字符串 * * @param limit 限制的长度 * @return 指定长度的字符串 */ public static String readString(int limit) { return readKeyBoard(limit, false); } /** * 功能:读取键盘输入的指定长度的字符串或默认值,如果直接回车,返回默认值,否则返回字符串 * * @param limit 限制的长度 * @param defaultValue 指定的默认值 * @return 指定长度的字符串 */ public static String readString(int limit, String defaultValue) { String str = readKeyBoard(limit, true); return str.equals("") ? defaultValue : str; } /** * 功能:读取键盘输入的确认选项,Y或N * 将小的功能,封装到一个方法中. * * @return Y或N */ public static char readConfirmSelection() { System.out.println("请输入你的选择(Y/N): 请小心选择"); char c; for (; ; ) {//无限循环 //在这里,将接受到字符,转成了大写字母 //y => Y n=>N String str = readKeyBoard(1, false).toUpperCase(); c = str.charAt(0); if (c == 'Y' || c == 'N') { break; } else { System.out.print("选择错误,请重新输入:"); } } return c; } /** * 功能: 读取一个字符串 * * @param limit 读取的长度 * @param blankReturn 如果为true ,表示 可以读空字符串。 * 如果为false表示 不能读空字符串。 * <p> * 如果输入为空,或者输入大于limit的长度,就会提示重新输入。 * @return */ private static String readKeyBoard(int limit, boolean blankReturn) { //定义了字符串 String line = ""; //scanner.hasNextLine() 判断有没有下一行 while (scanner.hasNextLine()) { line = scanner.nextLine();//读取这一行 //如果line.length=0, 即用户没有输入任何内容,直接回车 if (line.length() == 0) { if (blankReturn) return line;//如果blankReturn=true,可以返回空串 else continue; //如果blankReturn=false,不接受空串,必须输入内容 } //如果用户输入的内容大于了 limit,就提示重写输入 //如果用户如的内容 >0 <= limit ,我就接受 if (line.length() < 1 || line.length() > limit) { System.out.print("输入长度(不能大于" + limit + ")错误,请重新输入:"); continue; } break; } return line; } }
-
测试Utility类
-
-
HouseRentApp.java
程序入口, 直接创建对象调用方法源码:
package com.yxz.house; import com.yxz.house.view.HouseView; public class HouseRentApp { public static void main(String[] args) { // 创建HouseView对象,并显示主菜单 -> 程序(系统)入口 new HouseView().mainMenu(); } }
第二阶段
第二阶段 目标:提升编程能力
1、面向对象编程(高级)
1.1、类变量和类方法
类变量和类方法使用static
关键字修饰,也称静态变量(静态属性)
和静态方法
。类变量是该类的所有对象共享的变量,任何一个该类的对象去访问它时,取到的都是相同的值,同样任何一个该类的对象去修改它时,修改的也是同一个变量。
定义类变量/类方法的语法:
访问修饰符 static 数据类型 变量名;// 推荐使用
static 访问修饰符 数据类型 变量名;
访问修饰符 static 数据返回类型 方法名() {} // 推荐使用
static 访问修饰符 数据返回类型 方法名() {}
如何访问类变量/类方法:(静态变量/静态方法的访问修饰符的访问权限和范围与普通成员一样,遵守访问规则)
类名.类变量名; // 推荐使用
对象名.类变量名;
类名.类方法名(); // 推荐使用
对象名.类方法名();
特点:
- 类变量和类方法会被该类的所有的对象实例共享。
- static类变量在类加载时就生成并初始化,即使没有创建对象,只要类加载了,就可以使用类变量。
- 在JDK8之前的版本中,静态变量是存放在方法区的静态域中;在JDK8之后的版本中,方法区中的类信息加载后会在堆中开辟属于该类的一个Class对象实例,属于该类的静态变量会存放在Class对象实例的尾部。
- 类变量的生命周期是随着类的加载开始,随着类消亡而销毁。
- 类方法和普通方法都是随着类的加载而加载,将结构信息存储在方法区。
- 类方法中不能使用和对象有关的关键字,如:
this
,super
。 - 类方法中只能访问静态变量或静态方法。
- 普通成员方法既可以访问非静态变量(方法),也可以访问静态变量(方法)。
- 静态方法可以被子类继承,但是不能被子类重写(覆盖) -> 静态方法属于类成员。
何时使用类变量/类方法:
- 当一个变量需要该类所有对象实例共享时,使用类变量。
- 当方法中不涉及到任何和对象相关的成员,可以将方法设计成静态方法,提高开发效率 -> 不希望创建对象实例,也可以调用方法时(当作工具使用)。
1.2、理解main方法语法
public static void main(String[] args){}
说明:
-
main()方法是Java虚拟机调用。
-
Java虚拟机需要调用类的main()方法 -> main()方法的访问权限必须是public
。 -
Java虚拟机在执行main()方法时不必创建对象 -> main()方法必须是static
。 -
main()方法接收String类型的数组参数,该数组中保存执行Java命令时所传递给运行的类的参数。
-
语法:
java 执行的程序(带有main()方法的程序) 参数1 参数2 参数3...
。如图: -
在main()方法中,可以直接调用main()方法所在类的静态方法和静态属性。但是不能直接访问该类的非静态成员,要创建该类的对象实例去访问。
-
在IDEA中,传入main()方法的参数:
构建 -> 编辑配置 -> 对应程序
1.3、代码块
代码化块又称为初始化块
,属于类中的成员 -> 类的一部分
,类似于方法,将逻辑语句
封装在方法体中,通过{}
包装起来。
但和方法不同,没有方法名,没有返回值类型,没有参数,只有方法体,而且不能通过对象或类显示调用,而是在加载类或创建对象时隐式调用。
基本语法:
[修饰符] {
代码;
};
说明:
- 修饰符要么static,要么不写。
- 代码块分为两类。使用static修饰的叫静态代码块,没有static修饰的叫普通代码块。
- 代码可以为任何逻辑语句(输入、输出、方法调用、控制结构等)。
- 结尾的
;
可写可不写。
代码块的好处:
- 相当于另外一种形式的构造器(对构造器的补充机制),可以做初始化的操作。
- 如果多个构造器中都有重复的语句,可以抽取到初始化块中,提高代码的重用性。
- 代码块的调用优先于构造器,不管调用哪个构造器,都会先调用代码块的内容。
注意:
-
static代码块的作用是对类进行初始化,
static代码块随着类的加载而执行且只会执行一次
。如果是非静态代码块,每创建一个对象就执行一次
。 -
类什么时候被加载:
- 创建对象实例时(
new
)。 - 创建子类对象实例时,父类也会被加载。
使用类的静态成员时
(静态变量、静态方法、静态代码块)。
- 创建对象实例时(
-
非静态代码块,在创建对象实例时会被隐式的调用
。如果只是使用类的静态成员时,普通代码块并不会执行
。 -
创建一个对象实例时,在一个类的调用顺序:静态(代码块、属性) -> 非静态(代码块、属性) -> 构造器
调用静态代码块和静态属性初始化
。静态代码块和静态属性初始化调用的优先级一样
,如果有多个静态代码块和多个静态变量初始化,则按照从上往下定义的顺序调用。调用普通代码块和普通属性的初始化
。普通代码块和普通属性初始化调用的优先级一样,如果有多个普通代码块和多个普通属性初始化,按照从上往下定义的顺序调用。调用构造方法
。
package com.yxz.oop_high; public class CodeBlock { public static void main(String[] args) { // 假设以下每次运行时只执行一个输出语句 // System.out.println(A.age); /* 分析:加载一次类信息,访问静态成员,不创建对象实例 1. 调用静态代码块,输出:A的静态代码块 被调用 2. 调用静态变量,输出:100 */ // System.out.println(new A().name); /* 分析:加载一次类信息,创建对象实例,访问非静态成员 1. 加载类信息,调用静态代码块,输出:A的静态代码块 被调用 2. 创建对象实例,调用A类的普通代码块,输出:A的非静态代码块 被调用 3. 调用A的构造器,实例化对象,输出:A的构造器 被调用 4. 访问普通成员,输出:abc */ // System.out.println(B.salary); /* 分析:加载一次类信息,调用静态成员,不创建对象实例 1. 先调用父类的静态代码块,完成父类初始化,输出:A的静态代码块 被调用 2. 加载类信息,调用本类的静态代码块,完成子类初始化,输出:B的静态代码块 被调用 3. 访问静态变量,输出:1314.52 */ System.out.println(new B().count); /* 分析:加载一次类信息,创建对象实例,访问非静态成员 1. 先完成父类信息的加载,调用父类的静态代码块,输出:A的静态代码块 被调用 2. 完成子类信息的加载,调用本类的静态代码块,输出:B的静态代码块 被调用 3. 需要创建对象实例,先完成父类非静态信息加载,输出:A的非静态代码块 被调用 4. 要创建子类对象,先调用父类构造器完成父类的初始化,输出:A的构造器 被调用 5. 需要创建本类对象实例,先完成本类的非静态信息加载,输出:B的非静态代码块 被调用 6. 调用完成子类构造器,完成子类对象初始化,输出:B的构造器 被调用 7. 访问本类的非静态信息,输出:10 */ } } class A { public static int age = 100; // 静态变量 static { // this.age = 120; // 错误,静态代码块内不能使用this/super和对象有关的关键字 System.out.println("A的静态代码块 被调用..."); } public String name = "abc"; { // 非静态代码块在创建对象实例时会被调用 System.out.println("A的非静态代码块 被调用..."); } public A() { // 默认隐藏的语句 // 1. super(); // 2. 普通代码块和普通属性的初始化; System.out.println("A的构造器 被调用..."); } } class B extends A { public static double salary = 1314.520; // 静态变量 static { System.out.println("B的静态代码块 被调用..."); } public int count = 10; { System.out.println("B的非静态代码块 被调用..."); } public B() { // 默认隐藏的语句 // 1. super(); // 2. 普通代码块和普通属性的初始化; System.out.println("B的构造器 被调用..."); } }
-
创建一个子类对象时(继承关系),他们的静态代码块、静态属性初始化、普通代码块、普通属性初始化、构造器的调用顺序:
- 父类的静态代码块和静态属性(优先级一样,按定义顺序执行)。
- 子类的静态代码块和静态属性(优先级一样,按定义顺序执行)。
- 父类的普通代码块和普通属性初始化(优先级一样,按定义顺序执行)。
- 父类的构造器。
- 子类的普通代码块和普通属性初始化(优先级一样,按定义顺序执行)。
- 子类的构造器。
package com.yxz.oop_high; public class CodeBlockDetail { public static void main(String[] args) { new B02(); } } class A02 { //父类 private static int n1 = getVal01(); static { System.out.println("A02的一个静态代码块..");//(2) } public int n3 = getVal02();//普通属性的初始化 { System.out.println("A02的一个普通代码块..");//(5) } public A02() {//构造器 //隐藏 //super() //普通代码和普通属性的初始化...... System.out.println("A02的构造器");//(7) } public static int getVal01() { System.out.println("A02的静态方法getVal01");//(1) return 10; } public int getVal02() { System.out.println("A02的普通方法getVal02");//(6) return 10; } } class B02 extends A02 { // 子类 private static int n3 = getVal03(); static { System.out.println("B02的一个静态代码块..");//(4) } public int n5 = getVal04(); { System.out.println("B02的一个普通代码块..");//(9) } //一定要慢慢的去品.. public B02() {//构造器 //隐藏了 //super() //普通代码块和普通属性的初始化... System.out.println("B02的构造器");//(10) } public static int getVal03() { System.out.println("B02的静态方法getVal03");//(3) return 10; } public int getVal04() { System.out.println("B02的普通方法getVal04");//(8) return 10; } }
-
静态代码块只能调用静态成员,普通代码块可以调用任意属性。
1.4、单例设计模式
设计模式(共23种):在大量的实践中总结和理论化之后优选的代码结构、编程风格以及解决问题的思考方式。
单例(单个的实例)模式(singleton pattern):采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法。
单例模式(8种)
的其中两种方式:饿汉式、懒汉式。
-
饿汉式
:在类中创建静态对象 -> 当加载类信息时就会自动创建该对象。即有可能还不需要使用该对象,但是类对象已经创建完毕 -> 很着急的饿汉实现步骤:
- 构造器私有化 -> 防止用户使用
new
创建对象。 - 类的内部创建对象。为了能够在静态方法中访问该对象,需要使用static修饰该对象 ->
该类的内部对象是static的
- 向外暴露一个
静态的公共方法getInstance,返回一个类的对象实例
。
代码实现:
package com.yxz.oop_high.single_; /** * 实现饿汉式的单例模式 */ public class SingleTon01 { public static void main(String[] args) { // System.out.println(GirlFriend.n1); //当访问类变量时,就已经加载了类信息 -> 静态对象已经创建完毕 GirlFriend girlFriend1 = GirlFriend.getInstance(); GirlFriend girlFriend2 = GirlFriend.getInstance(); System.out.println(girlFriend1 == girlFriend2); // true,是同一个对象地址 } } /** * 只能有一个女朋友(单例模式) */ class GirlFriend { // public static int n1 = 100; // 为了保证在静态方法getInstance()中能够返回girlFriend对象,要用static修饰 private static GirlFriend girlFriend = new GirlFriend("宝贝"); // 第二步:在类的内部直接创建 private String name; // 如何保证用户只能创建一个 GirlFriend 对象 // 步骤[单例模式-饿汉式] // 1.构造器私有化 // 2.在类的内部直接创建 // 3.提供一个公共的static方法,返回一个GirlFriend对象 private GirlFriend(String name) { // 第一步:构造器私有化 // System.out.println("构造器被调用..."); this.name = name; } public static GirlFriend getInstance() { // 第三步:提供一个公共的static方法,返回一个GirlFriend对象 return girlFriend; // 返回一个对象 } @Override public String toString() { return "GirlFriend{" + "name='" + name + '\'' + '}'; } }
饿汉式缺点:当访问类的其他静态成员时,不需要创建该类对象,但是该类静态对象已经在第一次加载类信息时已经创建完毕 -> 可能造成已经创建了对象,但是没有使用的问题。
- 构造器私有化 -> 防止用户使用
-
懒汉式:现在类中声明一个静态对象属性但不实例化对象,在公共的静态方法中创建对象 -> 只有使用getInstance方法时才创建对象 -> 懒汉
实现步骤:
- 构造器私有化 -> 防止用户使用
new
创建对象。 - 类的内部定义一个静态属性对象但不实例化。
- 向外暴露一个
静态的公共方法getInstance,创建并返回一个类的对象实例
。只有第一次调用该方法时才创建对象,否则还返回上次创建的对象。
代码实现:
package com.yxz.oop_high.single_; /** * 实现懒汉式的单例模式 */ public class SingleTon02 { public static void main(String[] args) { // System.out.println(BoyFriend.n1); // 访问其他类成员时,加载类信息但不会创建对象 BoyFriend boyFriend1 = BoyFriend.getInstance(); System.out.println(boyFriend1); BoyFriend boyFriend2 = BoyFriend.getInstance(); System.out.println(boyFriend1 == boyFriend2); // true; } } /** * 只能有一个男朋友(单例模式) */ class BoyFriend { // public static int n1 = 100; private static BoyFriend boyFriend; // 第二步:定义一个static静态属性对象,此时对象置空 private String name; // 步骤[单例模式-懒汉式] // 1.构造器私有化 // 2.定义一个static静态属性对象 // 3.提供一个公共的static方法,可以放回一个BoyFriend对象 private BoyFriend(String name) { // 第一步:构造器私有化 // System.out.println("构造器被调用..."); this.name = name; } public static BoyFriend getInstance() {// 第三步:提供一个公共的static方法,返回一个GirlFriend对象 if (boyFriend == null) { // 判断是否创建了对象 boyFriend = new BoyFriend("亲爱的"); } return boyFriend; } @Override public String toString() { return "BoyFriend{" + "name='" + name + '\'' + '}'; } }
- 构造器私有化 -> 防止用户使用
饿汉式和懒汉式的区别:
- 创建对象时机不同。饿汉式是在类加载就创建了对象实例,懒汉式是在使用时才创建。
- 饿汉式不存在线程安全问题,懒汉式存在线程安全问题。
- 饿汉式存在资源浪费的可能,懒汉式不存在资源浪费问题。
- 在JavaSE标准类中,
java.lang.Runtime
是经典的单例模式。
1.5、final 关键字
final
关键字可以修饰类、属性、方法和局部变量
。
适用场景(作用):
- final修饰类:该类无法被继承 -> final修饰的类没有子类。
- final修饰方法:该方法无法被子类覆盖/重写(override)。
- final修饰类的属性:该属性的值无法被修改。
- final修饰局部变量:该局部变量的值无法修改。
注意:
-
final修饰的属性又叫常量属性,一般用XX_XX_XX来命名(全部大写字母)。
-
final修饰的属性必须赋初值,并且以后不能在修改,赋值可以在如下位置:
- 定义时赋值。
- 在普通代码块中。
- 在构造器中。
-
如果final修饰的属性是静态的,则初始化的位置只能是:
- 定义时。
- 在
静态代码块中
。
-
final类不能被继承,但是可以实例化对象。
-
如果类不是final类,但是含有final方法,则该final方法虽然不能重写,但是可以被继承(遵守继承机制)。
-
一般地,如果一个类是final类,则没必要将该类的方法修饰成final方法 -> final类无子类,不会重写方法。
-
final不能修饰构造器。
-
final和static往往搭配使用,效率更高,调用final static修饰的成员时不会导致类加载,底层编译器做了优化处理。
public class Final01 { public static void main(String[] args) { System.out.println(A.n1); // 输出内容:10000 } } class A { public final static int n1 = 10000; static { System.out.println("A 的静态代码块被执行"); // 不会执行该语句 } }
-
包装类(
Integer
、Double
、Float
、Long
、Boolean
、String
、Byte
等)都是final类
,不能被继承 ->包装类没有子类
。
1.6、抽象类
当父类的某些方法,需要声明并让子类继承,但是又不确定方法如何具体实现时
,可以将其声明为抽象方法(abstract method),由子类重写实现该方法
,那么这个类就是抽象类(abstract class)
。
说明:
-
使用
abstract
关键字来修饰一个类时,该类就叫抽象类。抽象类的基本语法:
访问修饰符 abstract 类名{}
-
使用
abstract
关键字来修饰一个方法时,该方法就叫抽象方法 ->抽象方法没有方法体
。基本语法:
访问修饰符 abstract 返回值类型 方法名(参数列表);
-
抽象类的价值更多作用在于设计,设计者设计好后让子类继承并实现抽象类。
-
抽象类是面试官常问知识点,在框架和设计模式使用较多。
注意:
-
抽象类不能被实例化 -> 不能创建抽象类的对象。
-
抽象类不一定要包含抽象方法,还可以有实现的方法,->
抽象类可以没有abstract方法
。 -
抽象方法一定在抽象类里面 ->
一旦类包含了抽象方法,则这个类必须声明为abstract
。 -
abstract只能修饰类和方法
,不能修饰属性和其他的。 -
抽象类可以有任意成员(
抽象类本质还是类
),比如:非抽象方法、构造器、静态属性等。 -
抽象方法不能有方法体。
-
如果一个类继承了抽象类,则它必须实现抽象类的所有抽象方法,除非这个类也声明为abstract类。所谓实现方法就是有方法体。
-
抽象方法不能使用private、final和static来修饰
,因为这些关键字都是和方法重写机制相违背。private修饰的方法不能被子类访问 -> 子类无法重写父类private方法 -> 和abstract方法要求子类继承并重写实现方法的本意违背
final修饰的方法不能被子类继承 -> 子类无法继承父类final方法 -> 和abstract方法要求子类继承并重写实现方法的本意违背
static修饰的方法属于类方法且不能被子类重写
-> 不创建类对象也能使用类名调用的方法 -> 和abstract方法没有方法体相悖
抽象类的最佳实践-模板设计模式
任务需求:
- 有多个类,完成不同的任务job
- 要求能够得到各自完成任务的时间。
方案一:基本思维流程:在不同类的job方法里面计算时间
A.java
类文件,完成任务1
package com.yxz.oop_high.abstract_;
// 任务1:计算1+2+...+100000000所花费的时间
public class A {
public void job() {
// 得到开始的时间
long start = System.currentTimeMillis();
long num = 0;
for (int i = 1; i < 100000000; i++) {
num += i;
}
// 得到结束的时间
long end = System.currentTimeMillis();
System.out.println("执行的时间: " + (end - start));
}
}
B.java
类文件,完成任务2
package com.yxz.oop_high.abstract_;
// 任务2:计算1*2*...*100所花费的时间
public class B {
public void job() {
// 得到开始的时间
long start = System.currentTimeMillis();
long num = 1;
for (int i = 1; i < 10000000; i++) {
num *= i;
}
// 得到结束的时间
long end = System.currentTimeMillis();
System.out.println("执行的时间: " + (end - start));
}
}
方案二:将方案一的代码重复部分抽取出来封装成单独的方法calculateTimes(),然后在calculateTimes()方法中调用job()方法 -> 提高代码复用性
A.java
类文件,完成任务1
package com.yxz.oop_high.abstract_;
// 任务1:计算1+2+...+100000000所花费的时间
public class A {
// 代码改进方案一:将重复的代码抽出来封装成一个新方法,在新方法中调用计算任务job,如下
public void calculateTimes() {
long start = System.currentTimeMillis();
// 调用任务
job();
long end = System.currentTimeMillis();
System.out.println("执行的时间: " + (end - start));
}
public void job() {
long num = 0;
for (int i = 1; i < 100000000; i++) {
num += i;
}
}
}
B.java
类文件,完成任务2
package com.yxz.oop_high.abstract_;
// 任务2:计算1*2*...*100所花费的时间
public class B {
// 代码改进方案一:将重复的代码抽出来组成一个新方法,在新方法中调用计算任务job
public void calculateTimes() {
long start = System.currentTimeMillis();
// 调用任务
job();
long end = System.currentTimeMillis();
System.out.println("执行的时间: " + (end - start));
}
public void job() {
long num = 1;
for (int i = 1; i < 10000000; i++) {
num *= i;
}
}
}
方案三:在方案二的基础上,抽象一个父类Template,在父类声明一个抽象方法job(),将子类的封装方法calculateTimes()放在父类中,并在该方法中调用抽象方法job()。根据继承机制,当子类重写实现了父类的抽象方法job(),也会继承父类的calculateTimes()方法,可以直接调用该方法实现模板设计模式。
Template.java
模板类文件(父类)
package com.yxz.oop_high.abstract_;
// 模板类
abstract public class Template {
public abstract void job(); // 抽象方法,由子类重写实现
public void calculateTimes() { // 实现的方法
long start = System.currentTimeMillis();
job(); // 调用抽象方法,运行时会动态绑定子类重写的方法!!!!
long end = System.currentTimeMillis();
System.out.println("执行的时间: " + (end - start));
}
}
A.java
类文件,子类 完成任务1的job()方法实现
package com.yxz.oop_high.abstract_;
// 任务1:计算1+2+...+100000000所花费的时间
public class A extends Template {
@Override
public void job() { // 重写了父类的抽象方法
long num = 0;
for (int i = 1; i < 100000000; i++) {
num += i;
}
}
}
B.java
类文件,完成任务2
package com.yxz.oop_high.abstract_;
// 任务2:计算1*2*...*100所花费的时间
public class B extends Template {
@Override
public void job() { // 重写了父类的抽象方法
long num = 1;
for (int i = 1; i < 10000000; i++) {
num *= i;
}
}
}
Abstract01.java
测试类
package com.yxz.oop_high.abstract_;
public class Abstract01 {
public static void main(String[] args) {
A a = new A();
a.calculateTimes(); // 动态绑定机制
B b = new B();
b.calculateTimes(); // 动态绑定机制
}
}
1.7、接口
接口(Interface)就是给出一些没有实现的方法,封装到一起,到某个类要使用的时候,根据具体情况把这些方法写出来。
接口的基本语法:
interface 接口名 {
属性
方法(1.抽象方法 2.默认实现方法 3.静态方法)
}
class 类名 implements 接口名 {
本类属性
本类方法
必须实现接口的所有抽象方法
}
说明:
- 在JDK7.0之前 接口里的所有方法都没有方法体 -> 都是抽象方法
- 在JDK8.0之后接口可以有
静态方法
,默认方法(需要使用default关键字修饰)
,即接口中只有静态方法和默认方法有方法体。 - 在接口中的抽象方法可以省略
abstract
关键字。
注意:
-
接口不能被实例化
。 -
接口中的所有方法是public方法
(可以省略public关键字),接口中抽象方法可以不用abstract修饰。 -
一个普通类实现接口,就必须将该接口的所有方法都实现。在IDEA中,将光标停在实现接口的类名上,使用
Alt + Enter
快捷键可以快速实现接口所有方法。 -
抽象类实现接口,可以不用实现接口的方法
。 -
一个类可以同时实现多个接口。
-
接口中的属性只能是public static final属性
,而且属性必须初始化。如:int n1 = 1;
=等价于=>public final static int n1 = 1
。 -
接口中属性和静态方法的访问形式:
接口名.属性名
,接口名.方法名()
-
通过实现类的对象,调用接口中的默认方法。如果实现类重写了接口中的默认方法,则调用时还是调用重写后的默认方法(和继承的重写一致)。
interface A { int n = 10; // 等价于 public final static int n = 10; static void hi() { System.out.println("hi"); } public default void method() { // 默认方法必须有default关键字 System.out.println("默认方法1"); } default void method2() { // 省略了public关键字,权限依然是public System.out.println("默认方法2"); } } public class Test implements A { public static void main(String[] args) { A.hi(); System.out.println(A.n); } }
-
接口不能继承其他的类,但是可以继承多个别的接口。接口与接口之间使用
extends
继承,接口与类之间使用implements
实现。interface A {} interface B {} interface C extends A, B{} // 接口继承不能使用implements
-
接口的修饰符只能是public和默认,这点和类的修饰符一样。
接口 VS 继承
接口是对Java中单根继承机制的补充。
-
继承
当子类继承父类之后,
子类就自动拥有父类的能力
即父类所有(可以访问的)属性和方法。(先天拥有的能力)继承的价值主要在于:解决代码的复用性和可维护性。
继承需要满足
is-a
的关系。 -
接口
当
子类需要扩展功能,可以通过实现接口的方式来扩展
。(后天学习拥有的能力)接口的价值主要在于:设计,设计好各种规范(方法),让其它类实现这些方法。
接口比继承更加灵活,只需要满足
like-a
的关系。接口在一定程度上实现代码解耦[
接口的规范性+动态绑定机制
]。
接口的多态特性
-
多态参数
接口引用可以指向实现了接口的类的对象实例
->接口名 接口类型的引用名 = new 实现接口的类名();
案例:手机类和相机类实现UsbInterface,可以将phone对象和camera对象传入接口引用.
UsbInterface.java
接口文件package com.yxz.oop_high.interface_; public interface UsbInterface { // 规定接口的相关方法 public void start(); public void stop(); }
Camera.java
类文件package com.yxz.oop_high.interface_; public class Camera implements UsbInterface { // 实现接口的所有抽象方法 @Override public void start() { System.out.println("相机开始工作..."); } @Override public void stop() { System.out.println("相机停止工作..."); } }
Phone.java
类文件package com.yxz.oop_high.interface_; // Phone类 实现UsbInterface -> 需要实现UsbInterface接口 规定/声明的方法 public class Phone implements UsbInterface { // 实现接口方法 @Override public void start() { System.out.println("手机开始工作..."); } @Override public void stop() { System.out.println("手机停止工作..."); } }
Computer.java
类文件,创建work(UsbInterface usbInterface)方法 ->根据传入的不同实现接口的类的对象,调用不同的方法 -> 接口的多态
package com.yxz.oop_high.interface_; public class Computer { // 编写一个方法,计算机工作 public void work(UsbInterface usbInterface) { // 将设备接入到计算机 // 通过接口,来调用方法 -> 接口的多态 usbInterface.start(); // 动态绑定机制:根据传入的不同实现接口的类的对象,调用不同的start方法 usbInterface.stop(); // 动态绑定机制:根据传入的不同实现接口的类的对象,调用不同的stop方法 } }
Interface01.java
测试类package com.yxz.oop_high.interface_; public class Interface01 { public static void main(String[] args) { // 创建对象 // Phone 实现了 UsbInterface 接口 Phone phone = new Phone(); // Camera 实现了 UsbInterface 接口 Camera camera = new Camera(); // 创建计算机 Computer computer = new Computer(); computer.work(phone); // 将手机接入计算机,然后工作 -> 多态+动态绑定机制 System.out.println("================="); computer.work(camera); // 将相机接入计算机,然后工作 -> 多态+动态绑定机制 // 接口的多态 // 接口类型的变量 phone2 可以指向实现了UsbInterface接口类的对象实例 UsbInterface phone2 = new Phone(); UsbInterface camera2 = new Camera(); } }
-
多态数组
案例:在Usb(接口)数组中,存放Phone和Camera对象,Phone类有一个特有方法call()。遍历Usb数组,如果是Phone对象,除了调用Usb接口定义的方法work()外,还要调用特有方法call()。
public class InterfacePloyArr { public static void main(String[] args) { // 接口数组,多态数组 Usb[] usbs = new Usb[2]; usbs[0] = new Phone(); // 接口多态 usbs[1] = new Camera(); // 接口多态 for(int i = 0; i < usbs.length; i++) { usbs[i].work(); // 动态绑定 // 进行类型判断,向下转型 if (usbs[i] instanceof Phone) { // 判断其运行类型是否为Phone ((Phone)usbs[i]).call(); // 向下转型 } } } } interface Usb { void work(); } class Phone implements Usb { // 特有方法call public void call() { System.out.println("手机可以打电话..."); } // 实现接口的方法 @Override public void work() { System.out.println("手机工作中..."); } } class Camera implements Usb { // 实现接口的方法 @Override public void work() { System.out.println("相机工作中..."); } }
-
接口存在多态传递现象
如果一个类实现了的接口继承了其他接口,那么该类相当于也实现了其他接口
。如下:public class InterfacePloyPass { public static void main(String[] args) { // 接口类型的变量可以指向实现了该接口的类的对象实例 IG ig = new Teacher(); // 如果IG 继承了 IH 接口,而Teacher类实现了 IG接口 // 那么 就相当于Teacher也实现了IH接口 -> 接口的多态传递现象 IH ih = new Teacher(); // 虽然Teacher类没有实现IH接口,但是IG接口继承了IH接口 } } interface IH { void hi(); } interface IG extends IH {} // 接口继承 class Teacher implements IG { @Override public void hi() {}// 要求必须实现接口的方法 }
1.8、内部类
内部类(inner class):一个类的内部又完整的嵌套了另一个类结构,被嵌套的类就是内部类(inner class),嵌套其他类的类称为外部类(outer class)。是类的第五大成员(类的五大成员:1.属性 2.构造器 3.成员方法 4.代码块 5.内部类
)。内部类最大的特点就是可以直接访问外部类的私有属性,并且可以体现类与类之间的包含关系。
基本语法:
class Outer { // 外部类
class Inner { // 内部类
}
}
class Other { // (外部)其他类
}
内部类的分类
-
定义在外部类的局部位置上,如:方法体内
-
局部内部类(有类名)
局部内部类的位置通常在方法体内,也可以在代码块中。
局部内部类本质上还是一个类 -> 也可以有类的五大成员
。特点:
-
可以直接访问外部类的所有成员,包括私有成员
。 -
不能添加访问修饰符
,因为它的地位就是一个局部变量
-> 局部变量不能用访问修饰符。但是可以用final修饰,不让其他内部类继承
, 也可以使用abstract
修饰成抽象局部内部类. -
作用域:仅仅在定义它的方法体内或代码块中
。 -
局部内部类访问外部类的成员的方式:直接访问。无需借助类名或对象。
-
外部类 -- 访问 --> 局部内部类的成员,方式:在含有局部内部类的方法中创建内部类对象,内部类对象调用内部类成员。在其他外部类创建该外部类对象然后调用含有内部类的方法。
-
外部其他类 --
不能访问
--> 局部内部类(地位局部变量)。 -
如果外部类和局部内部类的成员重名时,默认遵循就近原则,如果想访问外部类的成员,则可以使用
外部类名.this.成员
访问。外部类名.this
本质上是一个外部类对象,哪个对象调用了含有内部类的方法,外部类名.this
就指向了哪个对象。
局部内部类的测试代码
package com.yxz.oop_high.innerclass; public class LocalInnerClass { public static void main(String[] args) { Outer02 outer02 = new Outer02(); outer02.m1(); // 调用含有局部内部类的方法 System.out.println("outer02的hashCode = " + outer02); // 输出和内部类的方法中输出一样的hashCode -> 同一个对象 } } class Outer02 { // 外部类 private int n1 = 100; private int n2 = 300; public void m1() { // 方法 // 局部内部类是定义在外部类的局部位置,通常在方法体内 final class Inner02 { // 局部内部类(本质上还是一个类) private int n1 = 123; // 可以访问外部类的所有成员,包括私有的 public void f1() { System.out.println("n2 = " + n2); m2(); // 可以访问外部类的所有成员 System.out.println("内部类的n1 = " + n1); // 内部类和外部类的成员重名时,要访问外部类成员,使用外部类名.this.成员名 // Outer02.this本质就是外部类的对象,即哪个对象调用了含有内部类方法m1(),就指向哪个对象 System.out.println("外部类的n1 = " + Outer02.this.n1); System.out.println("Outer02.this的hashCode = " + Outer02.this); } } // 外部类在含有局部内部类的方法中可以创建局部内部类对象,然后调用方法即可 Inner02 inner02 = new Inner02(); inner02.f1(); // 调用内部类的方法 // class Inner03 extends Inner02{} // 无法继承final类 } private void m2() { System.out.println("m2()被调用..."); } }
-
-
匿名内部类(没有类名
,非常常用)匿名内部类本质:1.类 2.局部内部类(方法体中或者代码块中) 3.匿名,无法直接看到类名 4.是一个对象
匿名内部类的基本语法:
class Outer { // 外部类 public void method() { new 类名(参数列表) { // 基于类的匿名内部类 类体; }; new 接口() { // 基于接口的匿名内部类 类体; }; new 抽象类(参数列表) { // 基于抽象类的匿名内部类 类体; 必须实现抽象类的抽象方法; }; } }
匿名内部类的测试代码
package com.yxz.oop_high.innerclass; interface IA { void cry(); } /** * 匿名内部类的演示 */ public class AnonymousInnerClass { public static void main(String[] args) { Outer03 outer03 = new Outer03(); outer03.method(); outer03.method(); } } class Outer03 { private int n1 = 10; public void method() { // 方法 // 基于接口的匿名内部类 // 1.需求:想使用IA接口,并创建对象且只使用一次,不用类实现接口的方式 // 使用匿名内部类简化开发 // tiger的编译类型:IA // tiger的运行类型:匿名内部类:XXXX => Outer03$1 /* 匿名内部类Outer03$1的底层源码:由系统分配匿名内部类的类名 class Outer03$1 implements IA { @Override public void cry() { System.out.println("老虎叫..."); } } */ // JDK底层在创建匿名内部类Outer03$1时,立即马上创建了Outer03$1的对象实例,并且把对象地址返回给tiger // 匿名内部类使用一次就消失,不能再使用,但是接口引用指向的对象还存在 IA tiger = new IA() { @Override public void cry() { System.out.println("老虎叫..."); } }; System.out.println("tiger的运行类型:" + tiger.getClass()); tiger.cry(); // 基于类的匿名内部类 // 1. father的编译类型:Father // 2. father的运行类型:XXXX => 底层系统分配类名Outer03$2 // 3. 底层会创建匿名内部类Outer03$2 // 4. 同时返回了匿名内部类Outer03$2的对象实例给father // 5. 参数列表jack会自动传递给Father类的构造器 /* class Outer03$2 extends Father{ // 有继承关系 } */ Father father = new Father("jack") { @Override public void test() { // 动态绑定机制 System.out.println("匿名内部类重写了test方法"); } }; System.out.println("father的运行类型 = " + father.getClass()); father.test(); // 运行类型是匿名内部类Outer$2,调用test方法会动态绑定匿名内部类重写的test方法 // 基于抽象类的匿名内部类 // 编译类型:Animal // 运行类型:匿名内部类XXXX => 由系统底层分配类名Outer03$3 Animal animal = new Animal() { @Override void eat() { System.out.println("小狗狗吃东西"); } }; animal.eat(); /* 底层本质: class 匿名内部类 extends Person {} */ new Person() { // 匿名内部类本身也是返回对象 @Override public void hi() { System.out.println("子类重写的hi()方法,n1 = " + n1); } }.hi(); // 本质:匿名内部类的对象调用方法,动态绑定子类(匿名内部类Outer03$4)的hi()方法 } } class Father { public Father(String name) { System.out.println("Father构造器被调用,接收到name = " + name); } public void test() { } } abstract class Animal { abstract void eat(); }
注意:
- 匿名内部类既是一个类的定义,同时它本身也是一个对象,因此从语法上看,它既有定义类的特征,也有创建对象的特征。
创建匿名内部类的最后有一个分号;
相等于创建对象的语句。- 如果在匿名内部类重写了接口的方法或者父类的方法,在使用匿名内部类对象调用方法时会有动态绑定机制。
- Java底层在创建匿名内部类时,会给匿名内部类分配类名,创建完之后会立即返回一个匿名内部类对象。
- 可以访问外部类的所有成员,包含私有的。
- 不能添加访问修饰符,地位是局部变量。
- 作用域:仅仅在定义它的方法或代码块中。
- 外部其他类 -- 不能访问 --> 匿名内部类。
- 如果外部类和匿名内部类的成员同名时,匿名内部类要访问外部类的同名成员,方法和局部内部类一样:
外部类名.this.成员
访问
匿名内部类的实践:当作实参传递,更加简洁高效
案例一:
package com.yxz.oop_high.innerclass; public class InnerClassExercise01 { public static void main(String[] args) { test(new A() { // 将匿名内部类当作实参使用 -> 匿名内部类本质上返回一个对象 @Override public void show() { System.out.println("匿名内部类实现了接口的方法"); } }); // 传统方法(硬编码) -> 死板,不灵活 test(new Picture()); } static void test(A a) { a.show(); } } interface A { void show(); } // 传统方式 实现接口 => 编程领域的硬编码 class Picture implements A { @Override public void show() { System.out.println("一幅名画..."); } }
案例二:
package oop_high_Exercise; // 接口类 interface Computer { // work方法是完成计算,如何计算交给匿名内部类完成 double work(double d1, double d2); } public class Homework04 { public static void main(String[] args) { CellPhone cellPhone = new CellPhone(); cellPhone.testWork(new Computer() {// 将匿名内部类当成对象传递给方法 @Override public double work(double d1, double d2) { return d1 + d2; } }, 99, 999); cellPhone.testWork(new Computer() {// 将匿名内部类当成对象传递给方法 @Override public double work(double d1, double d2) { return d1 * d2; } }, 520, 1314); } } class CellPhone { // 当调用testWork方法时,直接传入一个实现了Computer接口的匿名内部类即可 // 该匿名内部类可以灵活的实现work方法,完成不同的计算任务 public void testWork(Computer computer, double d1, double d2) { double res = computer.work(d1, d2); // 动态绑定匿名内部类重写的方法 System.out.println("运算的结果 = " + res); } }
-
-
定义在外部类的成员位置上:
-
成员内部类(不用static修饰)
成员内部类是定义在外部类的成员位置,并且没有static修饰。
成员内部类的基本语法:
class Outer { // 外部类 class Inner { // 成员内部类 类体; } }
注意:
- 可以直接访问外部类的所有成员,包含私有属性。
- 可以添加任意访问修饰符,因为它的地位是类的成员。
- 作用域:和外部类其他成员一样,为整个类体。在外部类的成员方法中创建成员内部类对象,再调用方法。
- 成员内部类 -- 访问 --> 外部类,方式:直接访问
- 外部类 -- 访问 --> 内部类,访问方式:在外部类的成员方法中创建内部类对象,再调用方法。
- 外部其他类 -- 访问 --> 成员内部类,通过内部类对象访问,创建内部类对象的两种方式:
- 使用外部类对象new一个内部类对象,
外部类名.内部类名 引用 = 外部类对象.new 内部类名();
- 在外部类中编写一个方法,方法返回类型是内部类对象,接收该方法返回值得到内部类的对象实例。
- 使用外部类对象new一个内部类对象,
- 如果外部类和内部类的成员重名时,内部类访问遵循就近原则,如果内部类想要访问外部类的重名成员,使用
外部类名.this.成员
访问。
成员内部类的测试代码
package com.yxz.oop_high.innerclass; public class MemberInnerClass01 { public static void main(String[] args) { Outer05 outer05 = new Outer05(); outer05.t1(); // 外部其他类,使用成员内部类的三种方式 // 第一种方式:使用外部类对象new一个内部类对象 Outer05.Inner05 inner05 = outer05.new Inner05(); // 第二种方式:在外部类中编写一个方法,方法返回内部类对象 Outer05.Inner05 inner05Instance = outer05.getInner05Instance(); } } class Outer05 { private int n1 = 1000; private String name = "bbq"; private void hi() { System.out.println("hi()方法"); } // 成员方法 public void t1() { // 创建成员内部类对象 Inner05 inner05 = new Inner05(); inner05.say(); System.out.println(inner05.sal); } public Inner05 getInner05Instance() { return new Inner05(); } // 成员内部类 public class Inner05 { public double sal = 99.8; private int n1 = 5; public void say() { // 可以直接访问外部类的所有成员 System.out.println("n1 = " + n1 + " name = " + name); // 访问所有外部类成员 hi(); // 访问外部类重名成员,使用外部类名.this.成员访问 System.out.println("外部类成员n1 = " + Outer05.this.n1); } } }
-
静态内部类(使用static修饰)
静态内部类是定义在外部类的成员位置,并且有static修饰,地位相当于静态变量。
说明:
- 可以直接访问外部类的所有静态成员,包含私有的,但不能直接访问非静态成员。
- 可以添加任意访问修饰符。
- 作用域:同其他类成员,整个类体。
- 静态内部类 -- 访问 --> 外部类(静态属性),访问方式:直接访问。
- 外部类 -- 访问 --> 静态内部类,访问方式:在外部类的方法中创建内部类对象,再调用外部类方法。
- 外部其他类 -- 访问 --> 静态内部类,
- 如果外部类和静态内部类的成员重名时,静态内部类要访问外部类同名(静态)属性,使用
外部类名.属性
访问。静态内部类不能直接访问外部类非静态成员(需要创建外部类对象)。
静态内部类的测试代码:
package com.yxz.oop_high.innerclass; public class StaticInnerClass01 { public static void main(String[] args) { Outer06 outer06 = new Outer06(); outer06.m1(); // 外部其他类使用静态内部类, // 方式1:相当于调用类的静态变量,可以通过类名直接访问(遵守访问权限) Outer06.Inner06 inner06 = new Outer06.Inner06(); inner06.say(); // 方式2:编写一个(静态)方法,方法返回静态内部类的对象实例 Outer06.Inner06 inner06Instance = outer06.getInner06Instance(); // 外部类对象调用非静态方法 Outer06.Inner06 inner06Instance_ = Outer06.getInner06Instance_(); // 外部类调用静态方法 } } class Outer06 { private static String name = "张三"; private int n1 = 10; public static void cry() { } public void m1() { // 外部类访问静态内部类:先创建对象,在调用方法 Inner06 inner06 = new Inner06(); inner06.say(); } public Inner06 getInner06Instance() { return new Inner06(); } public static Inner06 getInner06Instance_() { return new Inner06(); } // 成员 静态内部类 static class Inner06 { public void say() { // System.out.println(n1); 错误,静态内部类不能直接访问外部类的非静态成员 System.out.println(name); // 可以访问外部类的所有静态成员 cry(); Outer06 outer06 = new Outer06(); } } }
-
2、枚举和注解
2.1、自定义类实现枚举
枚举(enumeration, 简写enum)是一组常量的集合。枚举属于一种特殊的类,里面只包含一组有限的特定的对象。
如何实现自定义枚举类:
- 构造器私有化 -> 防止直接创建对象。
- 去掉类的相关set方法 -> 防止属性值被修改。
- 在类的内部,直接创建静态对象。
- 优化,使用final关键字修饰静态对象。
package com.yxz.enum_;
public class Enumeration01 {
public static void main(String[] args) {
System.out.println(Season.SPRING);
}
}
class Season {
private final String name;
private final String desc; // 描述
// 定义四个固定的对象
public static final Season SPRING = new Season("春天", "温暖");
public static final Season SUMMER = new Season("夏天", "炎热");
public static final Season AUTUMN = new Season("秋天", "凉爽");
public static final Season WINTER = new Season("冬天", "寒冷");
private Season(String name, String desc) {
this.name = name;
this.desc = desc;
}
public String getName() {
return name;
}
public String getDesc() {
return desc;
}
@Override
public String toString() {
return "Season{" +
"name='" + name + '\'' +
", desc='" + desc + '\'' +
'}';
}
}
2.2、enum关键字实现枚举
步骤:
- 使用
enum
关键字替代class
。 - 将创建静态常量对象的语句改成
常量名(实参列表);
,相当于简化了代码,底层还是会调用构造器。 - 如果有多个常量(对象),使用
,
间隔即可。 - 如果使用enum实现枚举,需要将常量对象写在最前面。
package com.yxz.enum_;
public class Enumeration02 {
public static void main(String[] args) {
System.out.println(Season.SPRING);
System.out.println(Season.SUMMER);
Season a = Season.SPRING;
Season b = Season.SPRING;
System.out.println(a == b); // true,同一个静态常量对象
}
}
enum Season { // 枚举类
// 枚举对象写在最前面
SPRING("春天", "温暖"), SUMMER("夏天", "炎热"), AUTUMN("秋天", "凉爽"), WINTER("冬天", "寒冷");
private final String name;
private final String desc; // 描述
private Season(String name, String desc) {
this.name = name;
this.desc = desc;
}
public String getName() {
return name;
}
public String getDesc() {
return desc;
}
@Override
public String toString() {
return "Season{" +
"name='" + name + '\'' +
", desc='" + desc + '\'' +
'}';
}
}
注意:
- 当使用
enum
关键字实现一个枚举类时,默认会继承java.lang.Enum类
,而且枚举类会自动变成一个final类。使用javap指令反编译
查看源码。 - 传统方式实现枚举类中定义对象的语句
public static final Season SPRING = new Season("春天", "温暖");
简化成SPRING("春天", "温暖")
仍然会调用构造器 -> 枚举类需要提供构造器。 如果使用无参构造器创建枚举对象,则实参列表和小括号都可以省略
。- 当有多个枚举对象时,使用
,
间隔,最后有一个;
结尾。 枚举对象必须放在枚举类的行首
。- 使用enum实现的枚举类的所有构造器必须是
private
的,默认是private的 -> 构造器的修饰符可以不写
。 enum
实现的枚举类中的常量对象都是静态对象
-> 同一个常量对象被所有枚举类的对象共享。- 使用enum关键字后就不能再继承其他类,因为
enum修饰的类会隐式继承Enum类
,Java是单根继承继承,可以使用接口
。
枚举类和switch的搭配使用
基本语法:
switch (枚举类对象) {
case 枚举常量值1: 语句; break;
case 枚举常量值2: 语句; break;
case 枚举常量值3: 语句; break;
......
}
案例:枚举类Color,五种颜色的枚举对象,实现接口的show()方法,打印枚举对象的三个属性值。
package oop_high_Exercise;
enum Color implements ColorInterface {
RED(255, 0, 0),
BLUE(0, 0, 255),
BLACK(0, 0, 0),
YELLOW(255, 255, 0),
GREEN(0, 255, 0);
int redValue;
int greenValue;
int blueValue;
Color(int redValue, int greenValue, int blueValue) {
this.redValue = redValue;
this.greenValue = greenValue;
this.blueValue = blueValue;
}
@Override
public void show() {
System.out.println("redValue = " + redValue + ", greenValue = " + greenValue + ", blueValue = " + blueValue);
}
}
interface ColorInterface {
void show();
}
public class Homework08 {
public static void main(String[] args) {
Color green = Color.GREEN;
green.show();
switch (green) {
case RED -> System.out.println("红色");
case BLUE -> System.out.println("蓝色");
case BLACK -> System.out.println("黑色");
case GREEN -> System.out.println("绿色");
case YELLOW -> System.out.println("黄色");
default -> System.out.println("颜色错误");
}
}
}
enum常用方法
- toString():Enum类已经重写,返回的是当前对象名,子类可以重写该方法。
- name():返回当前常量对象(枚举对象)名,
子类不能重写该方法
。 - ordinal():返回的是该枚举对象的次序/编号,从0开始编号。
- values():返回当前枚举类所有枚举对象。
- valueOf(String str):根据传入的字符串去枚举类中匹配枚举对象名,如果匹配成功则返回对应的枚举对象,否则抛出异常。
- compareTo():比较两个枚举对象,比较的就是枚举对象的次序/编号(从0开始,ordinal()方法返回的编号),返回两个对象的编号差值。
2.3、JDK内置的基本注解类型
注解(Annotation)也被称为元数据(Metadata),用于修饰解释包、类、方法、属性、构造器、局部变量等数据信息。
注解和注释一样,注解不影响程序的逻辑,但注解可以被编译或运行
,相当于在代码中的补充信息。
在JavaSE中,注解的使用目的比较简单,如标记过时的功能,忽略警告等。在JavaEE中注解占据了更重要的角色,例如用来配置应用程序的任何切面,代替JavaEE旧版中所遗留的繁冗代码和XML配置等。
使用Annotation时要在其前面增加@符号,并把该Annotation当成一个修饰符使用,用于修饰它支持的程序元素。
-
@Override:限定某个方法,是重写父类方法,该注解只能用于方法(继承时已讲)。
添加@Override注解,
编译器会对该方法进行校验(从编译层面验证)
-> 判断该方法是否真的重写父类的方法,如果不构成重写,则编译错误。@Override的源码
:package java.lang; import java.lang.annotation.*; @Target(ElementType.METHOD) @Retention(RetentionPolicy.SOURCE) public @interface Override { }
说明:
@interface
不是interface
,表示一个注解类。是JDK5.0后加入的。@Target(ElementType.METHOD)
说明该@Override注解只能修饰方法。@Target
是修饰注解的注解,称为元注解。 -
@Deprecated:用于表示某个程序元素(类、方法等)已过时即不推荐使用,但是不代表不能使用。
@Deprecated class A { @Deprecated public int n1 = 10; @Deprecated public void hi(){} }
@Deprecated的源码
:package java.lang; import java.lang.annotation.*; import static java.lang.annotation.ElementType.*; @Documented @Retention(RetentionPolicy.RUNTIME) @Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, MODULE, PARAMETER, TYPE}) public @interface Deprecated { String since() default ""; boolean forRemoval() default false; }
@Deprecated
可以修饰构造器、字段、局部变量、方法、包、参数等等。可以做版本升级过度使用。 -
@Suppresswarnings:抑制编译器警告。
抑制的信息种类根据传入的参数决定。
@SuppressWarnings抑制警告的范围和其放置位置有关
public class Test { @SuppressWarnings("all") // 抑制所有警告信息 public static void main(String[] args) { int i = 0; int b = 10; System.out.println(b); } }
@SuppressWarnings的源码
:package java.lang; import java.lang.annotation.*; import static java.lang.annotation.ElementType.*; @Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, MODULE}) @Retention(RetentionPolicy.SOURCE) public @interface SuppressWarnings { String[] value(); // 传入数组,对多种类型的警告进行抑制 }
2.4、元注解
元注解是修饰注解的注解。
元注解种类:
-
@Retention
只能用于修饰一个注解,指定注解的作用范围,@Rentention包含一个RetentionPolicy类型的成员变量,使用时必须为该value成员变量指定值(SOURCE,CLASS,RUNTIME)
RetentionPolicy.SOURCE:编译器使用后,直接丢弃的注解。
RetentionPolicy.CLASS:编译器将注解记录在class文件中,当运行Java程序时,JVM不会保留注解(默认值)。
RetentionPolicy.RUTIME:编译器将把注解记录在class文件中,当运行Java程序时,JVM会保留注解,程序可以通过反射获取该注解。
-
@Target:指定注解可以在哪些地方使用。
-
@Documented:指定该注解是否会在javadoc体现。即在生成文档时,可以看到该注解。定义为Documented的注解必须设置Retention值为RUNTIME。
-
@Inherited:子类会继承父类注解。
3、异常
3.1、异常的概念
当程序出现了问题,不应该就终止运行,使用针对性的代码进行特殊处理(try-catch
)来保证程序即使出现异常也能正常运行 -> 保证程序的健壮性。
在IDEA中,将可能抛出异常的代码选中,使用快捷键:Alt+Shift+T
,选择try-catch
,可以快速生成异常处理try-catch
的代码。
在Java语言中,将程序执行中发生的不正常情况称为“异常”。(开发过程中的语法错误和逻辑错误不是异常)
执行过程中所发生的异常事件可分为两类:
- Error(错误):Java虚拟机无法解决的严重问题。如:JVM系统内部错误、资源耗尽等严重情况。比如:
StackOverflowError[栈溢出]和OOM(out of memory)
,Error是严重错误,程序会崩溃
。 - Exception:其他因
编程错误或偶然的外在因素
导致的一般性问题
,可以使用针对性的代码进行处理。例如:空指针访问
,试图读取不存在的文件
,网络连接中断
等等,Exception分为两大类:运行时异常(feishoujian程序运行时发生的异常)和编译时异常(编程时,编译器检查出的异常)
。
3.2、异常体系图
常见的异常,体现了继承和实现的关系:
总结:
- 异常分为两大类:非受检(unchecked)异常(运行时异常) 和 受检(checked)异常(编译时异常)。
- 运行时异常,编译器检查不出来。一般指编程时的逻辑错误,是程序员应该避免其出现的异常。
java.lang.RuntimeException类
及它的子类都是运行时异常。 - 对于运行时异常,可以不做处理,因为这类异常很普遍,若全处理可能会对程序的可读性和运行效率产生影响。
- 编译时异常,是编译器要求必须处置的异常,否则代码无法编译通过。
3.3、常见的异常
常见的运行异常:
NullPointerException
:空指针异常
当应用程序试图在需要对象的地方使用null
时,抛出该异常。
ArithmeticException
:数学运算异常
当出现异常的运算条件时,抛出此异常。如:除数为0时,抛出该异常。
ArrayIndexOutOfBoundsException
:数组下标越界异常
用非法索引访问数组时抛出的异常。如果索引为负或大于等于数组大小,则该索引为非法索引,抛出索引越界异常
ClassCastException
:类型转换异常
当试图将对象强制转换为不是实例的子类时,抛出该异常。
NumberFormatException
:数字格式不正确异常
当程序试图将字符串转换成一种数值类型,但该字符串不能转换为适当格式时,抛出该异常。使用该异常可以确保输入是满足条件数字。
常见的编译异常:
SQLException
:操作数据库时,查询表可能发生异常。
IOException
:操作文件时,发生的异常。
FileNotFoundException
:当操作一个不存在的文件时,发生异常。
ClassNotFoundException
:加载类,而该类不存在时,抛出该异常。
EOFException
:操作文件,到文件末尾,发生异常。
IllegalArgumentException
:参数异常。
3.4、异常处理
异常处理就是当发生异常时,对异常处理的方式。
方式一:使用try-catch-finally
:程序员在代码中捕获发生的异常,自行处理。如果没有finally代码块也可以 -> try-catch
处理异常。
处理机制示意:
try {
可能有异常的代码块;
} catch(Exception e) {
当捕获到异常后,执行catch代码块的内容
1.系统将异常封装成Exception对象e,传递给catch代码块
2.得到异常对象后,程序员自己处理
3.如果没有发生异常,则catch代码块不会执行
} finally {
1.不管代码块是否有异常发生,始终要执行finally代码块的内容
2.通常将释放资源的操作代码放在finally代码块中
}
注意:
- 如果异常发生了,则异常发生后面的try块中的代码不再执行,直接进入到catch块。
- 如果异常没有发生,则顺序执行try块,不会进入catch块。
- finally块用于执行不管是否发生异常都需要执行的操作。
- 如果代码块
可能有多个异常
,可以使用多个catch分别捕获不同的异常进行进行处理。(要求:子类异常在前捕获,父类异常后捕获)
- 可以进行
try-finally
配合使用,相当于没有捕获异常,执行完finally块后程序会直接中断。应用场景:执行一段代码,不管是否发生异常,都必须执行某个业务逻辑的代码。
package com.yxz.exception_;
public class Exception01 {
public static void main(String[] args) {
try {
int[] arr = {1, 2, 3};
for (int i = 0; i <= arr.length; i++) {
System.out.println(arr[i]); // ArrayIndexOutOfBoundsException
}
String str = "Mi cai";
str = null;
System.out.println(str.length()); // NullPointerException
int n1 = 10;
int n2 = 0;
int result = n1 / n2; // ArithmeticException
} catch (NullPointerException e) {
System.out.println("处理了空指针异常");
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("处理了数组索引越界异常");
} catch (ArithmeticException e) {
System.out.println("处理了数学算数异常");
} catch (Exception e) {
System.out.println("未知异常");
} finally {
System.out.println("释放了资源");
}
}
}
方式二:使用throws
:将发生的异常抛出,交给调用者(方法)来处理,最顶级的处理者是JVM。
如果一个方法可能生成某种异常,但是并不能确定如何处理这种异常,则此方法应显示地声明抛出异常
,表明将该方法将不对这些异常进行处理,而由该方法的调用者负责处理。
在方法声明中用throws语句可以声明抛出异常的列表,throws后面的异常类型可以是方法中产生的异常类型,也可以是它的父类。throws
关键字后也可以是异常列表,即可以抛出多个异常。
public class Throws01 {
public static void main(String[] args) {
}
public void f1() throws FileNotFoundException, NullPointerException, ArithmeticException {
FileInputStream fis = new FileInputStream("d://a.txt");
}
}
注意:
- 对于编译异常,程序中必须处理,比如try-catch或者throws。
- 对于运行异常,程序中如果没有处理,默认throws方式处理。
- 子类重写父类的方法时,对抛出异常的规定:子类重写的方法,所抛出的异常类型要么和父类抛出的异常一致,要么为父类抛出的异常的类型的子类型。
- 在throws过程中,如果有方法try-catch,就相当于处理异常,就可以不必throws。
- 在Java中,如果抛出运行异常,并不要求程序员显式处理,有默认处理机制throws。
3.5、自定义异常
当程序中出现了某些“错误”,但该错误信息并没有在Throwable子类中描述处理,就可以自己设计异常类,用于描述错误信息。
自定义异常的步骤:
- 定义类:自定义异常类名,继承Exception或RuntimeException
- 如果继承Exception,属于编译异常。
- 如果继承RuntimeException,属于运行异常(一般情况下,让自定义异常继承RuntimeException,即做成运行异常 -> 可以使用默认处理机制)。
public class CustomException {
public static void main(String[] args) {
int age = 180;
if (!(age >= 18 && age <= 120)) {
throw new AgeException("年龄需要在18-120之间");
}
System.out.println("你的年龄正确");
}
}
class AgeException extend RuntimeException {
public AgeException(String message) {
super(message);
}
}
3.6、throw和throws 的对比
throws:异常处理的一种方式,用于方法声明处,后面紧跟异常类型。
throw:手动生成异常对象的关键字,用于方法体中,后面跟异常对象。
3.7、面试题:finally块和return的执行顺序
-
try块中有
return 基本数据类型;
public static int testReturn1() { int i = 1; try { i++; System.out.println("try:" + i); return i; } catch (Exception e) { i++; System.out.println("catch:" + i); } finally { i++; System.out.println("finally:" + i); } return i; /* 输出: try:2 finally:3 2 */ }
说明:
当try中带有return时,会先执行return前的代码
,然后暂时保存需要return的信息,再执行finally中的代码
,最后再通过return返回之前保存的信息。所以,这里方法返回的值是try中计算后的2,而非finally中计算后的3。 -
try块中有
return 引用类型;
public static List<Integer> testReturn2() { List<Integer> list = new ArrayList<>(); try { list.add(1); System.out.println("try:" + list); return list; } catch (Exception e) { list.add(2); System.out.println("catch:" + list); } finally { list.add(3); System.out.println("finally:" + list); } return list; /* 输出: try:[1] finally:[1, 3] [1, 3] */ }
说明:return时会临时保存需要返回的信息,不受finally中的影响,为什么这里会有变化?其实问题出在参数类型上,上一个例子用的是基本类型,这里用的引用类型。list里存的不是变量本身,而是变量的地址,所以
当finally通过地址改变了变量,还是会影响方法返回值的
。 -
try块中有异常,catch块中有
return 引用类型;
public static int testReturn3() { int i = 1; try { i++; System.out.println("try:" + i); int x = i / 0; } catch (Exception e) { i++; System.out.println("catch:" + i); return i; } finally { i++; System.out.println("finally:" + i); } return i; /* 输出: try:2 catch:3 finally:4 3 */ }
说明:catch中return与try中一样,会先执行return前的代码,然后暂时保存需要return的信息,再执行finally中的代码,最后再通过return返回之前保存的信息。所以,这里方法返回的值是try、catch中累积计算后的3,而非finally中计算后的4。
-
try块中有异常,finally块中有
return 引用类型;
public static int testReturn4() { int i = 1; try { i++; System.out.println("try:" + i); return i; } catch (Exception e) { i++; System.out.println("catch:" + i); return i; } finally { i++; System.out.println("finally:" + i); return i; } /* 输出: try:2 finally:3 3 */ }
说明:
当finally中有return的时候,try中的return会失效
,在执行完finally的return之后,就不会再执行try中的return。这种写法,编译是可以编译通过的,但是编译器会给予警告,所以不推荐在finally中写return,这会破坏程序的完整性,而且一旦finally里出现异常,会导致catch中的异常被覆盖。
总结:
- finally中的代码总会被执行。
- 当try、catch中有return时,也会执行finally。return的时候,要注意返回值的类型,是否受到finally中代码的影响。
- finally中有return时,会直接在finally中退出,导致try、catch中的return失效。
4、常用类
4.1、包装类
针对八种基本定义相应的引用类型——包装类(Wrapper),有了类的特点就可以调用类中的方法。
基本数据类型 | 包装类及继承父类 |
---|---|
boolean | Boolean extends Object |
char | Character extends Object |
byte | Byte extends Number |
short | Short extends Number |
int | Integer extends Number |
long | Long extends Number |
float | Float extends Number |
double | Double extends Number |
包装类和基本数据的转换
- JDK5前的手动装箱和拆箱方式,装箱:基本类型 -> 包装类型,反之拆箱。
- JDK5及以后都是自动装箱和拆箱方式。
自动装箱底层调用的是valueOf方法
,如:Integer.valueOf()
public class Integer01 {
public static void main(String[] args) {
// jdk5前是手动装箱和拆箱
// 手动装箱 int -> Integer
int n1 = 100;
Integer integer = new Integer(n1); // 手动装箱
Integer integer1 = Integer.valueOf(n1); // 手动装箱
// 手动拆箱
// Integer -> int
int i = integer.intValue();
// jdk5之后,自动装箱和自动拆箱
int n2 = 200;
// 自动装箱 int -> Integer
Integer integer2 = n2; // 底层使用Integer.valueOf(n2)
// 自动拆箱
int n3 = integer2; // 底层使用integer.intValue()方法
Object obj1 = true ? new Integer(1) : new Double(2.0); // 三元运算符是一个整体,提升精度
System.out.println(obj1); // 1.0
}
}
包装类型和String类型的相互转换
public class Integer01 {
public static void main(String[] args) {
// 包装类(Integer) -> String
Integer i = 100;
// 方式1
String str1 = i + "";
// 方式2
String str2 = i.toString();
// 方式3
String str3 = String.valueOf(i);
// String -> 包装类(Integer)
String str4 = "12345";
Integer i2 = Integer.parseInt(str4);// 自动装箱
Integer i3 = new Integer(str4); // 构造器
}
}
包装类的面试题
-
Object obj1 = true ? new Integer(1) : new Double(2.0); // 三元运算符是一个整体,提升精度 System.out.println(obj1); // 1.0
三元运算符是一个整体,表达式的返回值精度由表达式1和表达式2的最高精度决定。
-
Integer i = new Integer(1); Integer j = new Integer(1); System.out.println(i == j); // false:使用new创造了对象,则判断对象地址 -> 不同 Integer m = 1; // 自动装箱 底层Integer.valueOf(1) -> 查看valueOf源码 -> 1在[-128, 127]之间,所以不创建对象 Integer n = 1; // 自动装箱 底层Integer.valueOf(1) -> 查看valueOf源码 -> 1在[-128, 127]之间,所以不创建对象 System.out.println(m == n); // true Integer x = 128; // 自动装箱 底层Integer.valueOf(1) -> 查看valueOf源码 -> 128不在[-128, 127]之间,所以创建对象 Integer y = 128; // 自动装箱 底层Integer.valueOf(1) -> 查看valueOf源码 -> 128不在[-128, 127]之间,所以创建对象 System.out.println(x == y); // false
valueOf的源码
:public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) // -128 ~ 127 return IntegerCache.cache[i + (-IntegerCache.low)]; // 调用静态方法并返回一个数组值,不创造对象 return new Integer(i); // 当i的值不在[-128, 127]之间,则返回新创建的对象 }
-
Integer x = 128; int y = 128; System.out.println(x == y); // true
注:只要有一个基本数据类型,
==
都是判断值是否相等。
4.2、String
说明:
- String实现了Serializable接口,说明String对象可以串行化,即String对象可以在网络上传输。
- String实现了Comparable接口,说明String对象可以进行比较。
String类的理解和创建对象
-
String对象用于保存字符串,也就是一组字符序列。
-
"jack"
是字符串常量,双括号括起的字符序列。 -
字符串的字符使用Unicode字符编码,一个字符(不区分字母和汉字)都占两个字节。
-
String是final类,即不能被继承。
-
String有属性
private final char value[]
:用于存放字符串内容。注:
value是一个final类型,不可以修改 -> value不可以指向新的地址,但是单个字符内容可以变化
。final char[] value = {'a', 'b', 'c'}; value[0] = 'H'; // 可以修改单个字符的值 char[] v2 = {'t', 'o', 'm'}; value = v2; // 错误:不可以更改final类型value的地址
String两种方式创建对象的区别:(⭐⭐⭐⭐⭐)
方式一:直接赋值String s = "yxz";
方式二:调用构造器String s2 = new String("yxz");
说明:
-
方式一:先从常量池查看是否有"yxz"数据空间,
如果有,直接让s指向该数据空间;如果没有则重新创建
,然后指向。s最终指向的是常量池的空间地址
。 -
方式二:
先在堆中创建空间,里面维护了value属性,指向常量池的"yxz"空间
。如果常量池没有"yxz",重新创建,如果有,直接通过value指向。s2最终指向的是堆中的空间地址
。 -
两种方式创建对象的内存布局示意图:
String字符串是不可变的。一个字符串对象一旦被分配,其内容不可变。
面试题:
-
String a = "hello" + "abc";
创建了几个对象?一个对象
。String a = "hello" + "abc";
编译器底层优化,等价于String a = "helloabc";
。 -
String a = "hello"; String b = "abc"; String c = a + b;
上述代码创建了几个对象?三个字符串常量对象("hello","abc","helloabc"),一个StringBuilder对象,一个String对象
说明:
String c = a + b;
底层是Stringbuilder sb = new StringBuilder(); sb.append(a); sb.append(b); String c = sb.toString(); sb是在堆中开辟的对象,并且append()是在原来字符串sb的基础上追加的。重要规则:String c1 = "ab" + "cd";常量相加,看的是常量池。 String c1 = a + b; 变量相加,底层使用StringBuilder是在堆中new了一个对象。
-
public class Test { final char[] ch = {'j', 'a', 'v', 'a'}; String str = new String("hsp"); public static void main(String[] args) { Test ex = new Test(); ex.change(ex.str, ex.ch); System.out.print(ex.str + " and "); System.out.println(ex.ch); } public void change(String str, char ch[]) { str = "java"; ch[0] = 'h'; } }
以上代码输出什么?输出:hsp and hava
内存分析图:
String常用方法:
- equals():区分大小写,判断内容是否相等
- equalsIgnoreCase():忽略大小写的判断内容是否相等
- length():获取字符的个数,字符串长度
- intern():返回字符串对象内容在常量池的地址。
- indexOf():获取字符在字符串中第一次出现的索引,索引从0开始,如果找不到,返回-1
- lastIndexOf():获取字符在字符串中最后一次出现的索引,索引从0开始,如找不到,返回-1
- substring():截取指定范围的字符串
- trim():去掉字符串的前后空格
- charAt():获取某索引处的字符,注:不能使用str[index]的方式获取
- toUpperCase():将字符串中所有字母都大写并返回。
- toLowerCase():将字符串中所有字母都小写并返回。
- concat():拼接字符串并返回
- replace():替换字符串中的字符
- split():分割字符串
- compareTo():比较字符串大小
- toCharArray():转换成字符数组
- format():格式字符串
4.3、StringBuffer
java.lang.StringBuffer
代表可变的字符序列,可以对字符串内容进行增删。
很多方法和String相同,但StringBuffer是可变长度的
。
StringBuffer是一个容器
。
StringBuffer的继承关系图:
-
直接父类AbstractStringBuilder中有属性
char[] value,不是final类型
,该value数组存放StringBuffer对象的字符串内容,不是final -> 可以修改字符串内容 ->value数组存放在堆中
。 -
StringBuffer实现了Serializable,即StringBuffer的对象可以串行化。
-
StringBuffer是final类,不能被继承。
-
StringBuffer保存的是字符串变量,里面的值可以更改,每次StringBuffer的更新实际上可以更新内容,不用每次更新地址(创建新对象),效率较高。
String每次更新都会创建新的对象(如果常量池中有字符串则不会创建)并更改地址。
-
StringBuffer创建对象时,默认给value数组设置大小为16,可以调用构造器显式指定其StringBuffer的大小 = str.length() + 16。
String -> StringBuffer 的两种方式
方式一:调用有参构造器StringBuffer(String str)
String str = "hello";
StringBuffer sb1 = new StringBuffer(str);
方式二:调用append(String str)方法
StringBuffer sb2 = new StringBuffer();
sb2 = sb1.append(str);
StringBuffer -> String 的两种方式
方式一:toString()方法
StringBuffer sb3 = new StringBuffer("hello");
String s = sb3.toString();
方式二:使用String的构造器传入StringBuffer对象
String s1 = new String(sb3);
StringBuffer类常用方法
- append():增加字符或字符串
- delete(int start, int end):删除索引[start, end)范围内的字符。
- replace(int start, int end, String str):替换索引[start, end)内的内容为字符(串)str
- indexOf():查找指定的子串在字符串中第一次出现的索引
- insert():在指定索引处插入字符(串)
- length():返回字符串长度。
4.4、StringBuilder
- 一个可变的字符序列。此类提供一个与StringBuffer兼容的API(两者方法一样),但
不保证同步(StringBuilder不是线程安全的)
。该类被设计用作StringBuffer的一个简易替换,用在字符串缓冲区被单个线程使用的时候
。在单线程中,建议优先使用该类,大部分情况下它比StringBuffer更快
。 - 在StringBuilder上的主要操作是append和insert方法,可重载这些方法,以接收任意类型的数据。
- StringBuilder继承AbstractSrtingBuilder类,实现了Serializable接口 -> StringBuilder对象可以串行化(可以网络传输、保存到文件)。
- StringBuilder是final类,不能被继承。
- StringBuilder的方法,没有做互斥的处理,即没有
synchronized
关键字 ->因此推荐在单线程情况下使用StringBuilder
。
String、StringBuffer、StringBuilder的比较
- StringBuilder和StringBuffer非常类似,均代表可变的字符序列,而且方法也一样。
- String:
不可变字符序列
,效率低,但是复用率高
-> 如果要对字符串做大量修改操作,不要使用String - StringBuffer:可变字符序列,
效率较高(增删),线程安全
。 - StringBuilder:可变字符序列,
效率最高,线程不安全
。
String、StringBuffer、StringBuilder的选择
- 如果字符串存在大量的修改操作,一般使用StringBuffer或StringBuilder。
- 如果字符串存在大量的修改操作,并在单线程的情况,使用StringBuilder。
- 如果字符串存在大量的修改操作,并在多线程的情况,使用StringBuffer。
- 如果字符串很少修改,被多个对象引用,使用String,如配置信息等。
4.5、Math
Math类常用方法:
- abs():绝对值
- pow(double a, double b):求a的b次幂
- ceil(double a):向上取整,返回大于等于a的最小整数
- floor(double a):向下取整,返回大于等于a的最大整数
- round(double/float a):四舍五入。
先给a+0.5
,再取整返回一个整型(long/int) - sqrt():求开方
- random():生成随机数。返回
0 <= x < 1
之间的一个随机小数。获取[a, b]之间的一个随机整数 公式:(int)(a + Math.random() * (b - a + 1))
- max():两个数的最大值
- min():两个数的最小值
4.6、Date、Calendar、LocalDate
第一代日期类Date(在java.util包下
)
-
Date精确到毫秒,代表特定的瞬间
Date类构造器:
- Date():获取当前系统的时间
- Date(long date):通过指定毫秒数date得到对应时间。
Date类的常用方法:
- getTime():获取某个时间对应的毫秒数。
-
SimpleDateFormat:格式和解析日期的类。它允许对日期进行格式化(日期 -> 文本)、解析(文本 -> 日期)和规范化。
创建SimpleDateFormat对象,可以指定相应的格式。格式中使用的字母是规定好的,不能乱写,格式字符规定:
SimpleDateFormat类构造器:
-
SimpleDateFormat(String pattern):pattern是根据规定格式字母组成的字符串
如:
SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 hh:mm:ss E");
SimpleDateFormat类的常用方法:
- format(Date date):将指定日期转换成指定格式的字符串
- parse(String s):
可以把一个格式化的String 转成对应的Date,得到的Date在输出时仍然是国外的方式
,如果希望指定格式输出,需要format转换。在把String转成Date时,使用的SimpleDateFormat格式需要和String的格式一样,否则会抛出转换异常ParseException。
示例代码:
import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; public class UseDate { public static void main(String[] args) throws ParseException { // 抛出异常 // 获取当前系统时间 Date date = new Date(); // 默认输出的日期格式是国外的方式,因此通常需要对格式进行转换 System.out.println("当前时间是:" + date); Date date1 = new Date(92733); // 通过指定毫秒数得到时间 System.out.println(date.getTime()); // 获取某个时间对应的毫秒数 // 创建SimpleDateFormat对象,根据字母规定,传入格式串 SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy年MM月dd日 hh:mm:ss E"); String format = simpleDateFormat.format(date); // 将日期转换成指定格式的字符串 System.out.println(format); String s = "2022年11月07日 05:20:00 周一"; Date parseDate = simpleDateFormat.parse(s); // 可能会抛出转换异常,需要抛出异常或者try-catch捕获异常 System.out.println("转换后的日期:" + simpleDateFormat.format(parseDate)); } }
-
第二代日期类Calendar(在java.util包下
)
public abstract class Calendar implements Serializable, Cloneable, Comparable<Calendar> {}
- Calendar类是一个抽象类,它为特定瞬间与一组诸如
YEAR
、MONTH
、DAY_OF_MONTH
、HOUR
等日历、字段之间的转换提供了一些方法,并为操作日历提供了方法。 - Calendar类的构造器是受保护的,获取其对象可以通过
getInstance()
方法。 - Calendar类提供大量的方法和字段给程序员,通过获取字段转化成时间。
- Calendar没有提供对应的格式化的类,需要程序员自己组合。
- 如果要获取24时进制的小时数,将
Calendar.HOUR -> Calendar.HOUR_OF_DAY
import java.util.Calendar;
public class Calendar_ {
public static void main(String[] args) {
Calendar c = Calendar.getInstance();
System.out.println(c);
System.out.println("年:" + c.get(Calendar.YEAR));
// Calendar返回月份是从0月开始编号
System.out.println("月:" + (c.get(Calendar.MONTH) + 1));
System.out.println("日:" + c.get(Calendar.DAY_OF_MONTH));
System.out.println("小时:" + c.get(Calendar.HOUR));
System.out.println("分钟:" + c.get(Calendar.MINUTE));
System.out.println("秒:" + c.get(Calendar.SECOND));
// 自己组合显示日期时间
System.out.println(c.get(Calendar.YEAR) + "-" + (c.get(Calendar.MONTH) + 1) + "-"
+ c.get(Calendar.DAY_OF_MONTH) + " " + c.get(Calendar.HOUR) + ":"
+ c.get(Calendar.MINUTE) + ":" + c.get(Calendar.SECOND));
}
}
第三代日期类
JDK 1.0中包含了java.util.Date类,但是它的大多数方法在JDK 1.1引入Calendar类后就被弃用。而Calendar类也存在问题:
- 可变性:像日期和时间这样的类应该是不可变的
- 偏移性:Date中的年份是从1900年开始的,而月份都是从0开始
- 格式化:格式化只对Date有用,Calendar没有格式化类
- Date类和Calendar类不是线程安全的,不能处理闰秒等(每隔2年,多出1秒)
为了解决以上问题,JDK 8.0引入第三代日期类
第三代日期类:
- LocalDate类(日期/年月日),在
java.time
包下 - LocalTime类(时间/时分秒),在
java.time
包下 - LocalDateTime类(日期时间/年月日时分秒),在
java.time
包下
LocalDateTime类常用方法:
-
now():静态方法,返货一个LocalDateTime类的对象
-
getYear():年
-
getMonth():英语月份
-
getMonthValue():数字月份
-
getDayOfMonth():日
-
getHour():小时(24进制)
-
getMinute():分钟
-
getSecond():秒
-
plusDays(long days):当前时间加上一个天数,返回一个将来的时间
-
plusYears(long years):当前时间加上年数,返回将来的时间。
-
minusWeeks(long weeks):当前时间减去周数,返回过去的时间
-
minusHours(long hours):当前时间减去小时数,返回过去的时间
-
等等,需要什么方法,
查阅JDK 8.0版本之后的API文档
在开发中,尽量使用第三代日期类。
DateTimeFormatter格式日期类(在java.time.format包下
)
类似于SimpleDateFormat类
DateTimeFormatter dtf = DateTimeFormatter.ofPattern(格式字符串);
String format = dtf.format(日期对象);
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
public class UseLocalDateTime {
public static void main(String[] args) {
LocalDateTime ldt = LocalDateTime.now();
System.out.println(ldt);
System.out.println("年:" + ldt.getYear());
System.out.println("月:" + ldt.getMonth()); // 英语月份
System.out.println("月:" + ldt.getMonthValue()); // 数字月份
System.out.println("日:" + ldt.getDayOfMonth());
System.out.println("时:" + ldt.getHour());
System.out.println("分:" + ldt.getMinute());
System.out.println("秒:" + ldt.getSecond());
LocalDate now = LocalDate.now(); // 可以获取年月日
LocalTime now1 = LocalTime.now(); // 可以获取时分秒
// 使用DateTimeFormatter对象 对 LocalDateTime对象进行格式化
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH小时mm分ss秒");
String format = dateTimeFormatter.format(ldt);
System.out.println("格式化的日期 = " + format);
LocalDateTime localDateTime = ldt.plusDays(999);
System.out.println("999天后是:" + dateTimeFormatter.format(localDateTime));
}
}
第三代日期类Instant(在java.time包下
)
- 通过Instant类的静态方法now()获取表示当前时间戳的对象。
- 通过Date类的form()方法,可以把Instant转成Date对象。
- 通过Date对象的toInstant()方法可以把date转成Instant对象。
import java.time.Instant;
import java.util.Date;
public class UseInstant {
public static void main(String[] args) {
Instant now2 = Instant.now();
System.out.println(now2);
Date from = Date.from(now2);
Instant instant = from.toInstant();
}
}
4.7、System
System类的常见方法:
-
exit():退出当前程序。exit(0)表示程序正常退出,0-正常状态。
-
arraycopy(Type[] src, int srcPos, Type[] des, int destPos, int length):复制数组元素,比较适合底层调用,一般使用Arrays.copyOf完成复制数组。参数说明:src源数组,srcPos从源数组的哪个索引位置开始拷贝,des目标数组,destPos把源数组src的数据拷贝到目标数组dest的哪个索引,length从源数组拷贝多少个数据到目标数组dest
int[] src = {1, 2, 3}; int[] dest = new int[3]; System.arraycopy(src, 0, dest, 0, src.length);
-
currentTimeMillis():返回当前时间距离1970年1月1日0时0分的毫秒数(返回值long型)
-
gc():运行垃圾回收机制System.gc();
4.8、Arrays
Arrays里面包含一系列静态方法,用于管理和操作数组。
Arrays类常用方法:
-
toString():返回数组的字符串形式。如:Arrrays.toString(arr)
-
sort():排序(自然排序和定制排序)
示例代码:
import java.util.Arrays; import java.util.Comparator; public class SortDetail { public static void main(String[] args) { Integer[] arr = {7, -1, 89, 53, 66}; /* 1.sort方法是重载的,可以通过传入一个接口Comparator实现定制排序 2.调用定制排序时,传入两个参数 (1)待排序的数组arr (2)实现了Comparator接口的匿名内部类,要实现接口的compare()方法 sort方法会调用TimSort类的private static <T> void binarySort(T[] a, int lo, int hi, int start, Comparator<? super T> c)方法 该方法的核心代码: while (left < right) { int mid = (left + right) >>> 1; if (c.compare(pivot, a[mid]) < 0) // 动态绑定匿名内部类实现的compare()方法 right = mid; else left = mid + 1; } 通过根据compare()方法的返回值是否大于0来确定排序的规则 上述代码充分体现了 接口编程 + 动态绑定 + 匿名内部类 的综合使用 */ Arrays.sort(arr, new Comparator<Integer>() { // 基于接口的匿名内部类,<Integer>表示泛型 @Override public int compare(Integer o1, Integer o2) { // 动态绑定机制,实现了接口的compare()方法 return o2 - o1; } }); for (Integer integer : arr) { System.out.print(integer + " "); } } }
案例:4本书排序,三种方式排序:(1)按照价格从高到低 (1)按照价格从低到高 (1)按照书名长度从大到小
import java.util.Comparator; public class BookSort { public static void main(String[] args) { BookSort test = new BookSort(); Book[] books = new Book[4]; books[0] = new Book("红楼梦", 100); books[1] = new Book("三国演义", 90); books[2] = new Book("Python从精通到跑路", 5); books[3] = new Book("Java从入门到放弃", 300); // 按价格从高到低排序 System.out.println("按价格从高到低排序"); test.sortBooks(books, new Comparator() { // 基于接口的匿名内部类 @Override public int compare(Object o1, Object o2) { // 动态绑定机制 Book book1 = (Book) o1; // 向下转型 Book book2 = (Book) o2; double priceMargin = book1.getPrice() - book2.getPrice(); // 因为方法返回int型,所以需要转换一下,正数和负数表示相反的排序顺序 if (priceMargin > 0) { return 1; } else if (priceMargin < 0) { return -1; } else { return 0; } } }); for (Book book : books) { System.out.println(book); } // 按价格从低到高排序 System.out.println("按价格从低到高排序"); test.sortBooks(books, new Comparator() { @Override public int compare(Object o1, Object o2) { Book book1 = (Book) o1; Book book2 = (Book) o2; double priceMargin = book2.getPrice() - book1.getPrice(); if (priceMargin > 0) { return 1; } else if (priceMargin < 0) { return -1; } else { return 0; } } }); for (Book book : books) { System.out.println(book); } // 按书名长度从大到小排序 System.out.println("按书名长度从大到小排序"); test.sortBooks(books, new Comparator() { @Override public int compare(Object o1, Object o2) { Book book1 = (Book) o1; Book book2 = (Book) o2; return book1.getName().length() - book2.getName().length(); } }); for (Book book : books) { System.out.println(book); } } /** * 书本冒泡排序方法 * * @param books : 数组 * @param c : 比较器接口 */ public void sortBooks(Book[] books, Comparator c) { // 传入接口 for (int i = 0; i < books.length - 1; i++) { for (int j = 0; j < books.length - 1 - i; j++) { if (c.compare(books[j], books[j + 1]) < 0) { // 动态绑定匿名内部类实现的compare()方法 Book book = books[j]; books[j] = books[j + 1]; books[j + 1] = book; } } } } } class Book { private String name; private double price; public Book(String name, double price) { this.name = name; this.price = price; } public String getName() { return name; } public void setName(String name) { this.name = name; } public double getPrice() { return price; } public void setPrice(double price) { this.price = price; } @Override public String toString() { return "Book{" + "name='" + name + '\'' + ", price=" + price + '}'; } }
-
binarySearch(Type[] arr, Type i):通过二分搜索法进行查找元素,要求必须在有序的数组中查找。如果元素i不存在则返回
-(low + 1)
。low指的是该元素在数组中应该存在的位置(索引)
。 -
copyOf(Type[] arr, int arr.length):数组元素的复制(底层调用System.arraycopy()方法)。从arr数组中,拷贝 arr.length个元素到newArr数组中。
如果拷贝的数组长度 > arr.length,就在新数组的后面增加null
如果拷贝的数组长度 < 0,就抛出异常NegativeArraySizeException
-
fill(Type[] arr, Type i):数组元素的填充。使用i替换掉arr数组中所有元素。
-
equals(Type[] arr1, Type[] arr2):比较两个数组元素内容是否完全一致。
-
asList():将一组值转换成list集合。
List<Integer> asList = Arrays.asList(2, 3, 4, 5, 6, 1); System.out.println("asList = " + asList); System.out.println("asList的运行类型:" + asList.getClass());
上述代码中,asList的编译类型:List接口,运行类型是List接口的实现子类Arrays一个内部类
java.util.Arrays$ArrayList
,是Arrays类的静态内部类:private static class ArrayList<E> extends AbstractList<E> implements RandomAccess, java.io.Serializable {}
4.9、BigInteger、BigDecimal
BigInteger大整数类,用于大数的运算。底层是将数当作字符串处理。对于BigInteger类型的数进行运算时不能直接使用运算符,需要调用其提供的方法运算。BigInteger类常用方法:
- add(BigInteger val):加法运算
- subtract(BigInteger val):减法运算
- multiply(BigInteger val):乘法运算
- divide(BigInteger val):除法运算
import java.math.BigInteger;
public class UseBigInteger {
public static void main(String[] args) {
BigInteger bigInteger1 = new BigInteger("999999999999999999999999999999999999999");
BigInteger bigInteger2 = new BigInteger("33333333333333333333333333");
BigInteger add = bigInteger1.add(bigInteger2); // 加法
System.out.println(add);
BigInteger subtract = bigInteger1.subtract(bigInteger2); // 减法
System.out.println(subtract);
BigInteger multiply = bigInteger1.multiply(bigInteger2); // 乘法
System.out.println(multiply);
BigInteger divide = bigInteger1.divide(bigInteger2); // 除法
System.out.println(divide);
}
}
BigDecimal类,用于高精度浮点型的运算。
BigDecimal类常用方法:
- add(BigDecimal val):加法运算
- subtract(BigDecimal val):减法运算
- multiply(BigDecimal val):乘法运算
- divide(BigDecimal val):除法运算,注意:当除法除不尽时,会出现无限循环小数,程序抛出ArithmeticException异常,需要指定保留精度。在方法中传入参数
BigDecimal.ROUND_CEILING
—> 除法结果保留到被除数的精度 -> divide(bigDecimal2, BigDecimal.ROUND_CEILING)
import java.math.BigDecimal;
public class UseBigDecimal {
public static void main(String[] args) {
BigDecimal bigDecimal1 = new BigDecimal("1234.1415926535201314");
BigDecimal bigDecimal2 = new BigDecimal("3.987653");
System.out.println(bigDecimal1.add(bigDecimal2)); // 加法
System.out.println(bigDecimal1.subtract(bigDecimal2)); // 减法
System.out.println(bigDecimal1.multiply(bigDecimal2)); // 乘法
// System.out.println(bigDecimal1.divide(bigDecimal2)); // 除法,可能会抛出异常(当除不尽时,无限循环小数)
System.out.println(bigDecimal1.divide(bigDecimal2, BigDecimal.ROUND_CEILING));
}
}
编程技巧
- 保证数据的合法性:当正确(错误)情况较少时,列举出所有正确(错误)情况然后整体取反。
- 处理非法数据:过关斩将,在开头把所有非法数据处理完再执行正确数据的代码。
案例1
package com.yxz.common_class.exercise;
import java.util.Scanner;
/**
* 输入用户名、密码、邮箱,如果信息录入正确,则提示注册成功,否则生成异常对象
* 要求:
* 1.用户名长度为2或3或4
* 2.密码的长度为6,要求全是数字
* 3.邮箱中包含@和. 并且@在.的前面
*/
public class Homework02 {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
String name = scanner.next();
String pwd = scanner.next();
String email = scanner.next();
try {
userRegister(name, pwd, email);
System.out.println("恭喜你,注册成功!!!");
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
public static void userRegister(String name, String pwd, String email) {
// 过关斩将,校验传入的数据是否正确
// 正确情况只有一种,写出正确情况再整体取反,简化代码
if (!(name != null && pwd != null && email != null)) {
throw new RuntimeException("参数不能为空");
}
// 第一关:用户名长度为2或3或4
int usernameLength = name.length();
// 正确情况只有三种,写出正确情况再整体取反,简化代码
if (!(usernameLength == 2 || usernameLength == 3 || usernameLength == 4)) {
throw new RuntimeException("用户名长度为2或3或4");
}
// 第二关:密码的长度为6,要求全是数字
// 正确情况只有一种,写出正确情况再整体取反,简化代码
if (!(pwd.length() == 6) && isDigit(pwd)) {
throw new RuntimeException("密码的长度为6,要求全是数字");
}
// 第三关:邮箱中包含@和. 并且@在.的前面
int i = email.indexOf('@');
int j = email.indexOf('.');
// 正确情况只有一种,写出正确情况再整体取反,简化代码
if (!(i > 0 && j > i)) {
throw new RuntimeException("邮箱中包含@和. 并且@在.的前面");
}
}
public static boolean isDigit(String pwd) {
char[] chars = pwd.toCharArray();
for (char aChar : chars) {
if (aChar < '0' || aChar > '9') {
return false;
}
}
return true;
}
}
案例2
package com.yxz.common_class.exercise;
import java.util.Scanner;
/**
* 编写程序,输入形式为:Zhang san Fen 的人名,以Fen,Zhang .S的形式打印出来
* 其中S是中间单词的首字母大写
*/
public class Homework03 {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
String name = scanner.nextLine();
printName(name);
}
public static void printName(String name) {
// 数据校验
if (name == null) {
System.out.println("名字不能为空");
return;
}
String[] names = name.split(" ");
if (names.length != 3) {
System.out.println("输入的姓名格式不对");
return;
}
String format = String.format("%s,%s .%c", names[2], names[0], names[1].toUpperCase().charAt(0));
System.out.println(format);
}
}
5、集合
本章节全是重点(⭐⭐⭐⭐⭐)
优点
- 可以动态保存任意多个对象,使用方便。
- 提供了一系列方便的操作对象的方法:add、remove、set、get等。
- 使用集合添加/删除新元素的代码更简洁
5.1、集合框架体系
很重要!!!(建议背诵)
集合框架体系图
字典框架体系图
集合主要分成两组(单列集合,双列集合)
Collection接口有两个重要的子接口List、Set,它们的实现子类都是单列集合。
Map接口的实现子类是双列集合,以键值对(Key-Value)的形式存放元素。
5.2、Collection接口
public interface Collection<E> extends Iterable<E> {}
Collection接口实现类的特点
- Collection实现子类可以存放多个元素,每个元素可以是Object
- 有些Collection的实现类,可以存放重复的元素,有些不可以
- 有些Collection的实现类,有些是有序的(List),有些不是有序的(Set)
- Collection接口没有直接的实现子类,是通过它的子接口Set和List来实现
Collection接口的常用方法:
-
add():添加单个元素
-
remove():删除指定元素。有重载方法,可以删除指定索引位置的元素,也可以删除指定元素
-
contains():查找元素是否存在
-
size():获取元素个数
-
isEmpty():判断是否为空
-
clear():清空
-
addAll():添加多个元素
-
containsAll():查找多个元素是否都存在
-
removeAll():删除多个元素
以ArrayList为例,测试Collection接口方法
package com.yxz.collection_;
import java.util.ArrayList;
import java.util.List;
public class UseCollection {
@SuppressWarnings({"all"})
public static void main(String[] args) {
List list = new ArrayList();
// add():添加单个元素
list.add(10); // 自动装箱为Integer
list.add("hello"); // 自动装箱为String
list.add(true); // 自动装箱为Boolean
System.out.println("list = " + list);
// remove():删除指定元素
list.remove(new Integer(10)); // 删除指定元素
list.remove(1); // 根据指定索引位置的元素
System.out.println("list = " + list);
// contains():查找元素是否存在
System.out.println("list是否包含\"hello\" = " + list.contains("hello"));
// size():获取元素个数
list.add(3.14);
list.add('X');
System.out.println(list.size());
// isEmpty():判断是否为空
System.out.println("list是否为空 = " + list.isEmpty());
// clear():清空
list.clear();
// addAll():添加多个元素
List list1 = new ArrayList();
list1.add(false);
list1.add("world");
list1.add("break");
list.addAll(list1);
System.out.println("list = " + list);
// containsAll():查找多个元素是否都存在
System.out.println("list是否包含list1所有元素 = " + list.containsAll(list1));
// removeAll():删除多个元素
list.removeAll(list1);
System.out.println(list);
}
}
Collection接口遍历元素方式
-
使用Iterator迭代器
Iterator对象称为迭代器,主要用于遍历Collection集合中的元素。
所有实现了Collection接口的集合类都有一个iterator()方法,用以返回一个实现了Iterator接口的对象,即可以返回一个迭代器。
Iterator仅用于遍历集合,Iterator本身并不存放对象。
执行原理
Iterator iterator = coll.iterator();
得到一个集合迭代器iterator调用方法hasNext()用来判断是否还有下一个元素
iterator调用方法next()指针下移,并将下移后集合位置上的元素返回
注意:在调用it.next()方法之前必须要调用it.hasNext()进行检测。如果不调用,且没有下一个元素,直接调用next()方法会抛出NoSuchElementException异常。
在IDEA中,快速生成包含迭代器的while循环 的快捷键:
itit
显示所有快速生成代码的快捷键:
Ctrl + J
,选择itit
生成while循环。 -
增强for循环
增强for循环,可以代替iterator迭代器
特点:增强for就是简化版的iterator,本质一样,只能用于遍历集合或数组。底层会调用以下源码:
public Iterator<E> iterator() { return new Itr(); } public boolean hasNext() { return cursor != size; }
基本语法:
for (元素类型 元素名 : 容器对象名) { 访问元素的语句 }
IDEA中,生成增强for循环的快捷键:
I
或者iter
5.2.1、List接口
public interface List<E> extends Collection<E> {}
List接口(在java.util包下
)是Collection接口的子接口
- List集合类中元素有序(即添加顺序和取出顺序一致)、且可重复。
- Lisr集合类中的每个元素都有其对应的顺序索引,即支持索引(从0开始)。
- List容器中的每个元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素。
- JDK API中List接口的实现类有很多,常用的有:
ArrayList
、LinkedList
、Vector
List集合添加了一些根据索引来操作集合元素的方法
:
- void add(int index, Object element):在index位置插入element元素
- boolean addAll(int index, Collection elements):在index位置开始将elements中的所有元素添加进来
- Object get(int index):获取指定index位置的元素
- int indexOf(Object obj):返回obj在集合中首次出现的位置
- int lastIndexOf(Object obj):返回obj在当前集合中最后一次出现的位置
- Object remove(int index):移除指定index位置的元素,并返回此元素
- Object set(int index, Object element):设置指定index位置的元素为element,相当于替换
- List subList(int fromIndex, int toIndex):返回区间[fromIndex, toIndex)内的所有元素构成的子集合
ArrayList类
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {}
说明:
可以加入null值,并且可以多个重复
。ArrayList底层使用数组来实现数据存储
。- ArrayList基本等同于Vector,除了
ArrayList是线程不安全(执行效率高)
,在多线程的情况下,不建议使用ArrayList。
注意:ArrayList底层结构和源码分析
测试代码,查看底层源码执行逻辑:
public class UseCollection {
@SuppressWarnings({"all"})
public static void main(String[] args) {
ArrayList list = new ArrayList();
// ArrayList list = new ArrayList(8);
// 使用for给list集合添加1-10的数据
for (int i = 1; i <= 10; i++) {
list.add(i);
}
// 使用for给list添加11-15的数据
for (int i = 11; i <= 15; i++) {
list.add(i);
}
list.add(16);
list.add(17);
list.add(18);
}
}
-
ArrayList中维护了一个Object类型的数组elementData。底层源码:
transient Object[] elementData;
transient
关键字表示瞬间,短暂的,被该关键字修饰的属性不会被序列化。 -
当创建ArrayList对象时,如果使用的是无参构造器,则初始化elementData容量为0,第1次添加,则扩容elementData为10,如果需要再次扩容,则扩容elementData为原来容量的1.5倍。(扩容机制:0 -> 10 -> (10的1.5倍)15 -> ...)
ArrayList类底层源码:
调用无参构造器创建对象时:创建了一个空的数组
public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
第一次给容器添加对象时:newCapacity = 10
public boolean add(E e) { modCount++; // 记录当前集合被修改的次数,防止多个线程同时修改该集合 add(e, elementData, size); return true; } private void add(E e, Object[] elementData, int s) { // s是传入的当前集合的大小 if (s == elementData.length) // 如果当前集合大小和容量相等,说明大小不够,要去扩容 elementData = grow(); // 扩容 elementData[s] = e; // 再赋值 size = s + 1; } private Object[] grow() { return grow(size + 1); // 调用grow()方法扩容 } private Object[] grow(int minCapacity) { // 该方法确定最小容量 int oldCapacity = elementData.length; if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { int newCapacity = ArraysSupport.newLength(oldCapacity, minCapacity - oldCapacity, /* minimum growth */ oldCapacity >> 1 /* preferred growth */); return elementData = Arrays.copyOf(elementData, newCapacity); } else { return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)]; } } public static int newLength(int oldLength, int minGrowth, int prefGrowth) { // assert oldLength >= 0 // assert minGrowth > 0 int newLength = Math.max(minGrowth, prefGrowth) + oldLength; // 1.5倍扩容 if (newLength - MAX_ARRAY_LENGTH <= 0) { return newLength; } return hugeLength(oldLength, minGrowth); }
第二次及其以后,按照1.5倍扩容。扩容时使用的方法
Arrays.copyOf(原数组, 新容量newCapacity)
,保证了原数组数据不丢失的情况下增加容量 -
如果使用的是指定大小的构造器,则初始elementData容量为指定大小,如果需要扩容,则直接扩容elementData为原来容量的1.5倍。(扩容机制:(指定大小)x -> 1.5*x -> 1.5*1.5*x -> ...)
底层源码
调用有参构造器
public ArrayList(int initialCapacity) { if (initialCapacity > 0) { this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { this.elementData = EMPTY_ELEMENTDATA; } else { throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); } }
第一次添加元素和扩容上面调用无参构造器添加元素、扩容的逻辑一致。
Vector类
public class Vector<E>
extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {}
说明:
- Vector底层也是一个对象数组,
protected Object[] elementData;
- Vector是线程同步的,即线程安全,Vector类的操作方法带有
synchronized
关键字 - 在开发中,
需要线程同步安全时,考虑使用Vector
底层结构 | 版本 | 线程安全(同步)效率 | 扩容倍数 | |
---|---|---|---|---|
ArrayList | 可变数组Object[] | JDK 1.2 | 不安全,效率高 | 如果有参构造则1.5倍扩,如果无参构造,第一次扩容10,从第二次开始1.5倍扩容 |
Vector | 可变数组Object[] | JDK 1.0 | 安全,效率低 | 如果无参,默认10,之后2倍扩。如果指定大小,容量不够时每次直接2倍扩容 |
Vector类底层源码
-
调用无参构造创建Vector对象时,会自动调用Vector的有参构造并传入初始大小10。
public Vector() { this(10); // 调用有参构造器,默认初始化大小为10 } public Vector(int initialCapacity) { this(initialCapacity, 0); } public Vector(int initialCapacity, int capacityIncrement) { super(); // 完成父类初始化 if (initialCapacity < 0) // false throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); this.elementData = new Object[initialCapacity]; // 创建数组,初始化容量为10 this.capacityIncrement = capacityIncrement; // capacityIncrement = 0 }
-
先判断是否需要扩容,再添加元素
public synchronized boolean add(E e) { modCount++; add(e, elementData, elementCount); return true; } private void add(E e, Object[] elementData, int s) { if (s == elementData.length) // false -> 当前容量足够 -> 不扩容 elementData = grow(); elementData[s] = e; elementCount = s + 1; } private Object[] grow() { return grow(elementCount + 1); } private Object[] grow(int minCapacity) { int oldCapacity = elementData.length; int newCapacity = ArraysSupport.newLength(oldCapacity, minCapacity - oldCapacity, /* minimum growth */ capacityIncrement > 0 ? capacityIncrement : oldCapacity /* preferred growth */); return elementData = Arrays.copyOf(elementData, newCapacity); } public static int newLength(int oldLength, int minGrowth, int prefGrowth) { // assert oldLength >= 0 // assert minGrowth > 0 int newLength = Math.max(minGrowth, prefGrowth) + oldLength; // 2倍扩容 if (newLength - MAX_ARRAY_LENGTH <= 0) { return newLength; } return hugeLength(oldLength, minGrowth); } private static int hugeLength(int oldLength, int minGrowth) { int minLength = oldLength + minGrowth; if (minLength < 0) { // overflow throw new OutOfMemoryError("Required array length too large"); } if (minLength <= MAX_ARRAY_LENGTH) { return MAX_ARRAY_LENGTH; } return Integer.MAX_VALUE; }
LinkedList类
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable {}
说明:
LinkedList实现了双向链表和双端队列特点
- 可以添加任意元素(元素可以重复),包括null
线程不安全
,没有实现同步
LinkedList的底层操作机制
LinkedList底层维护了一个双向链表
- LinkedList中维护了两个属性first和last分别指向首节点和尾节点
- 每个节点(Node对象),里面又维护了prev、next、item三个属性,其中通过prev指向前一个,通过next指向后一个节点。最终实现双向链表
- LinkedList的元素的添加和删除,不是通过数组完成的,相对来说效率较高
LinkedList类底层源码
-
当创建LinkedList对象时,先完成父类初始化,然后调用了LinkedList类的无参构造器,并不进行预扩容处理。
public LinkedList() {}
并初始化链表的首位指针:
transient Node<E> first; // 头指针,指向链表头结点 transient Node<E> last; // 尾指针,指向链表尾结点
-
每次添加元素都会将元素放在链表最后一个节点,第一次添加元素时,链表开辟第一个节点,该节点的首尾指向都指向null
public boolean add(E e) { linkLast(e); return true; } void linkLast(E e) { final Node<E> l = last; final Node<E> newNode = new Node<>(l, e, null); // 开辟新节点,并让新节点的next指针置空 last = newNode; // 让尾指针指向新节点 if (l == null) // 第一次添加元素是为true first = newNode; // 头指针指向第一个节点(头结点) else l.next = newNode; // 让链表最后一个节点的后向指针指向新开辟的节点 size++; modCount++; } Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; }
-
每次删除时,如果不指定删除哪个节点,则默认删除第一个节点
public E remove() { return removeFirst(); // 默认删除第一个节点 } public E removeFirst() { final Node<E> f = first;// 将f指向的双向链表的第一个节点拿掉 if (f == null) throw new NoSuchElementException(); return unlinkFirst(f); } private E unlinkFirst(Node<E> f) { // assert f == first && f != null; final E element = f.item; final Node<E> next = f.next; // 取出第一个节点的信息(后向指针,数据域) f.item = null; f.next = null; // help GC first = next; if (next == null) last = null; else next.prev = null; size--; modCount++; return element; } E unlink(Node<E> x) { // assert x != null; final E element = x.item; final Node<E> next = x.next; final Node<E> prev = x.prev; if (prev == null) { first = next; } else { prev.next = next; x.prev = null; } if (next == null) { last = prev; } else { next.prev = prev; x.next = null; } x.item = null; size--; modCount++; return element; }
LinkedList类的常用方法:
- add():添加元素到双向链表尾处
- remove():删除元素,无参默认删除第一个节点,重载方法可以指定删除索引处的节点
- set(int index, Type data):修改指定节点的数据域
- get():得到指定节点对象
- getFirst():获取双向链表的表头节点
ArrayList和LinkedList的比较:
底层结构 | 增删效率 | 改查效率 | |
---|---|---|---|
ArrayList | 可变数组 |
较低,数组扩容 |
较高 ,随机存取 |
LinkedList | 双向链表 |
较高 ,通过链表 追加 |
较低 ,顺序存取 |
如何选择ArrayList和LinkedList:
- 查找修改的操作多,选择ArrayList,支持随机存取
- 增加删除的操作多,选择LinkedList
- 一般地,在程序中,80%都是查询操作,大部分情况下选择ArrayList
5.2.2、Set接口
public interface Set<E> extends Collection<E> {}
Set接口说明:
无序(添加和取出的顺序不一致),没有索引
,取出的顺序不会变。不允许重复元素,所以最多包含一个null
- JDK API中Set接口的常用实现类:
HashSet
、LinkedHashSet
、TreeSet
- Set接口和List接口一样是Collection的子接口,因此常用方法和Collection接口一样(Set接口对象没有get方法)。
- Set接口和Collection的遍历方式一样,遍历方式可以用迭代器和增强for循环,但是
不能用索引的方式来获取元素
- Set接口的实现类的对象 ==> 简称:Set接口对象
HashSet类
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable{}
说明:
-
HashSet实现了Set接口
-
HashSet底层是HashMap,HashMap底层是数组+单向链表+红黑树。HashSet构造器源码:
public HashSet() { map = new HashMap<>(); }
-
可以存放null值,但是只能存放一个 -> 元素(对象)不能重复
-
HashSet不保证元素是有序的
,取决于hash后,再确定索引的结果 -> 存放元素的顺序和取出顺序不一定一致 -
在执行add()添加方法时,方法会返回一个boolean值,表示操作是否成功,当集合中该元素已经存在时,添加重复元素失败 -> 返回false
-
在执行remove(Object obj)时, 会
计算obj的hash值根据hash值去table表中找到相应的索引
, 然后删除该索引处的元素. -
IDEA中,使用快捷键
Alt+Insert
选择重写hashCode()和equals()方法
可以快速生成hashCode()和equals()方法。
HashMap底层机制
模拟实现HashMap(数组+链表)机制:
package com.yxz.collection_;
public class HashSetStructure {
@SuppressWarnings({"all"})
public static void main(String[] args) {
// 模拟实现HashSet底层(HashMap底层)
// 1.创建一个数组,数组类型Node[],也称为 表
Node[] table = new Node[16];
System.out.println("table = " + table);
// 2.创建节点
Node john = new Node("john", null);
table[2] = john;
Node jack = new Node("jack", null);
john.next = jack; // 将jack节点挂到john下
Node rose = new Node("Rose", null);
jack.next = rose; // 将rose节点挂到jack下
Node lucy = new Node("lucy", null);
table[3] = lucy; // 把lucy放到table表的索引为3的位置
System.out.println("table = " + table);
}
}
class Node { // 节点:存储数据,有后向指针,形成链表
Object item; // 存放数据
Node next; // 指向下一个节点
public Node(Object item, Node next) {
this.item = item;
this.next = next;
}
@Override
public String toString() {
return "Node{" +
"item=" + item +
", next=" + next +
'}';
}
}
分析HashSet添加元素底层如何实现?(hash() + equals()
)
- HashSet(HashMap)添加一个元素时,先得到hash值(
根据重写的hashCode()方法得到并进行特殊处理
),然后根据相应算法转成索引值。 - 找到存储数据表table,看这个索引位置是否已经存放元素。如果没有则直接加入。
如果有元素,则调用equals比较(根据对象所属类的equals()方法),如果相同,就放弃添加
,如果不相同,则添加以链表的形式到最后的位置。 - 在Java 8.0中,如果一条链表的元素个数 >=
static final int TREEIFY_THRESHOLD = 8
,并且table的大小 >=static final int MIN_TREEIFY_CAPACITY = 64
,就会进行树化(红黑树)。
分析HashSet的扩容和转成红黑树的机制?
- HashSet底层是HashMap,第一次添加时table数组扩容到16,$临界值(threshold) = 当前table数组容量 * 加载因子(loadFactor = 0.75) = 12$。
- 如果table数组使用到了临界值12,就会扩容到$16 * 2 = 32$,新的临界值就是$32 * 0.75 = 24$,依此类推...
- 在Java 8中,如果一条链表的元素个数到达
TREEIFY_THRESHOLD(默认值=8)
,并且table的大小 >=MIN_TREEIFY_CAPACITY(默认值=64)
,就会对满足上述条件的链表进行树化(红黑树),否则仍然采用数组扩容机制(每次2倍)。 - 当table表中的元素个数(包括所有链表上的元素)大于临界值,就进行数组扩容。
测试代码并对HashSet底层机制分析:
package com.yxz.collection_;
import java.util.HashSet;
public class HashSetSource {
@SuppressWarnings({"all"})
public static void main(String[] args) {
HashSet hashSet = new HashSet();
for (int i = 1; i <= 12; i++) {
hashSet.add(new A(i));
}
System.out.println("set = " + hashSet);
/*
源码解读:
1.执行HashSet()
public HashSet() {
map = new HashMap<>();
}
2.执行add()
public boolean add(E e) { // e = "java"
return map.put(e, PRESENT)==null; // (static) PRESENT = new Object();
}
3.执行put(),该方法会执行hash(key),得到key对应的hash值(此处hash值不完全等价于hashCode,有算法进行特殊处理),
算法是(h = key.hashCode()) ^ (h >>> 16); // 这里的hashCode()方法会根据程序员重写的hashCode()方法动态绑定
public V put(K key, V value) { // e = "java", value = PRESENT 共享
return putVal(hash(key), key, value, false, true);
}
4.执行putVal()
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i; // 定义了辅助变量
// table 就是HashMap的一个数组,类型是Node[]
// if语句表示如果当前table是null,或者大小 = 0,就是第一次扩容,16个空间
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// (1)根据key,得到hash 去计算该key应该存放到table表的哪个索引位置
// 并且把这个位置的对象,赋给辅助变量p
// (2)判断p是否为null
// (2.1)如果p为null,表示还没有存放过元素,将创建一个Node(key = "java", value = PRESENT)
// (2.2)就放在该位置tab[i] = newNode(hash, key, value, null);
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
// 开发技巧:定义辅助变量(局部变量)时,在需要辅助变量时再定义
Node<K,V> e; K k;
if (p.hash == hash && // 如果当前索引位置对应的链表的第一个元素和准备添加的key的hash值一样,并且满足以下条件之一的
// (1)准备加入的key 和 p指向的Node节点的key是同一个对象 (==判断对象地址是否相同)
// (2)p指向的Node节点的key(对象)的equals()方法 和 准备加入的key比较后相同 (重写的equals()方法判断对象是否相同)
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 再判断 p 是不是一棵红黑树
// 如果是一棵红黑树,就调用putTreeVal()来添加
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else { // 如果table对应索引位置已经是一个链表,就使用for循环比较
// (1)依次和该链表的每一个元素比较后都不相同,则加入到该链表的最后
注意:在把元素添加到链表后,立即判断:该链表是否已经有TREEIFY_THRESHOLD(8)个节点,
如果达到,就调用 treeifyBin(tab, hash);对当前满足8个节点的链表进行树化(转成红黑树)
注意:treeifyBin()方法在转成红黑树前,要再判断一次,判断条件:
// MIN_TREEIFY_CAPACITY = 64(默认)
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize(); // 如果一个链表满足8个节点,但是table表不足64时,先进行table表的扩容
// 如果table表有一个链表满足8个节点且table表容量 >= 64,则对该链表进行树化 -> 红黑树
// (2)依次和该链表的每一个元素比较过程中,如果有相同元素,就直接break退出循环放弃添加
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) { // 更新e,使e指针指向下一个节点
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e; // 更新p,让p指针保持和e指针同一指向
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold) // 当前table数组中的元素总个数大于临界值
resize(); // 扩容
afterNodeInsertion(evict);
return null;
}
*/
}
}
class A {
private int n;
public A(int n) {
this.n = n;
}
@Override
public int hashCode() {
return 100; // 返回同样hash值
}
}
案例:
- 编写一个Employee类,属性(name,sal,birthday),birthday是MyDate类
- 编写MyDate类有属性(year,month,day)。
- 在main方法中,创建HashSet对象,添加多个Employee对象,当name和birthday相同时认为是同一个对象,不能重复添加到HashSet对象中去。
package com.yxz.collection_;
import java.util.HashSet;
import java.util.Objects;
public class HashSetExercise {
@SuppressWarnings({"all"})
public static void main(String[] args) {
HashSet hashSet = new HashSet();
hashSet.add(new Employee("Mi cai", 500000.5, new MyDate(1997, 11, 12)));
hashSet.add(new Employee("Mi lan", 350000.7, new MyDate(1999, 10, 10)));
hashSet.add(new Employee("Mi cai", 500000.3, new MyDate(1997, 11, 12)));
for (Object o : hashSet) {
System.out.println(o);
}
}
}
class Employee {
private String name;
private double sal;
private MyDate birthday;
public Employee(String name, double sal, MyDate birthday) {
this.name = name;
this.sal = sal;
this.birthday = birthday;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Employee employee = (Employee) o;
return name.equals(employee.name) && birthday.equals(employee.birthday);
}
@Override
public int hashCode() {
return Objects.hash(name, birthday);
}
@Override
public String toString() {
return "Employee{" +
"name='" + name + '\'' +
", sal=" + sal +
", birthday=" + birthday +
'}';
}
}
class MyDate {
private int year;
private int month;
private int day;
public MyDate(int year, int month, int day) {
this.year = year;
this.month = month;
this.day = day;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MyDate myDate = (MyDate) o;
return year == myDate.year && month == myDate.month && day == myDate.day;
}
@Override
public int hashCode() {
return Objects.hash(year, month, day);
}
@Override
public String toString() {
return "MyDate{" +
"year=" + year +
", month=" + month +
", day=" + day +
'}';
}
}
LinkedHashSet类
public class LinkedHashSet<E>
extends HashSet<E>
implements Set<E>, Cloneable, java.io.Serializable {}
说明:
-
LinkedHashSet是HashSet的子类
(LinkedHashSet调用核心方法时,会直接调用父类HashSet的方法,所有原理一致,只是单向链表 -> 双向链表
)。 -
LinkedHashSet底层是一个LinkedHashMap
,底层维护了一个数组+双向链表
(LinkedHashSet有head头指针和tail尾指针,每一个节点都有before和after属性 -> 形成双向链表) -
LinkedHashSet根据元素的hashCode值来决定元素的存储位置,同时
使用双向链表维护元素的次序
,这使得元素看起来是以插入顺序保存的。 -
LinkedHashSet不允许重复元素
。 -
在LinkedHashSet中添加一个元素时,先求hash值,再确定索引,确定该元素在table表的位置,然后将添加的元素加入到双向链表中(如果已经存在,不添加。原理和HashSet一样)。添加时的代码逻辑:
tail.next = newElement; // 尾节点的后向next指针指向新节点 newElement.pre = tail; // 新节点的前向pre指针指向原尾节点 tail = newElement; // 原尾结点指向新节点
TreeSet类
public class TreeSet<E> extends AbstractSet<E>
implements NavigableSet<E>, Cloneable, java.io.Serializable {}
说明:
-
TreeSet底层是TreeMap
-
TreeSet存储单列key,其value值为静态常量对象
private static final Object PRESENT = new Object();
,TreeMap以key-value形式存储。 -
TreeSet在对key排序时,如果没有显式传入比较器,
TreeSet会进入TreeMap并将key对象转换成Comparable接口类型(需要该对象的类型实现Comparable接口), 底层源码:Comparable<? super K> k = (Comparable<? super K>) key;(向下转型) 如果不可转换则抛出类型转换异常(ClassCastException)
。即在没有传入比较器的情况下, TreeSet的key对象必须实现Comparable接口的compareTo()方法。 -
如果TreeSet的key是String类型的,创建TreeSet时不传入比较器,则默认按照字典序升序排序(字母a->z,A->Z),因为String实现了Comparable接口并重写了compareTo()方法。
-
想要自定义排序,需要调用有参构造器并传入一个比较器(匿名内部类)指定排序规则。底层会将该比较器赋给TreeSet父类TreeMap的属性
Comparator<? super K> cpr = comparator;
匿名内部类需要重写compare()方法,运行时会动态绑定到该方法。
-
如果当前要加入的key和一个已经存在的key值一样,则无法加入新key值。
-
key不能为null,否则会抛出空指针异常。
import java.text.ParseException;
import java.util.Comparator;
import java.util.TreeSet;
public class Test {
@SuppressWarnings({"all"})
public static void main(String[] args) throws ParseException {
// 1.当使用无参构造器,创建TreeSet时,仍然是无序的
// 2.使用TreeSet提供的一个构造器,可以传入一个比较器(匿名内部类)
// 制定排序规则
// TreeSet treeSet = new TreeSet();
TreeSet treeSet = new TreeSet(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
return ((String) o1).compareTo((String) o2);
}
});
treeSet.add("jack");
treeSet.add("tom");
treeSet.add("a");
treeSet.add("smith");
/*
1.调用含有比较器的TreeSet构造器
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator)); // TreeSet底层是TreeMap
}
2.将比较器传给TreeMap的属性
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
3.第一次添加时,会把k-v(v是常量)封装到Entry对象放入root
if (t == null) {
compare(key, key); // type (and possibly null) check
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
4.在调用add("tom")方法时,底层会执行到:
Comparator<? super K> cpr = comparator;
if (cpr != null) {
do {
parent = t;
// 动态绑定到匿名内部类(匿名对象)重写的compare()方法
cmp = cpr.compare(key, t.key);
if (cmp < 0)s
t = t.left;
else if (cmp > 0)
t = t.right;
else // 如果相等,返回0,key加入不了
return t.setValue(value);
} while (t != null);
}
5.如果没有显式传入比较器
else {
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
// key必须实现Comparable接口,否则抛出类型转换异常ClassCastException
Comparable<? super K> k = (Comparable<? super K>) key; // 向下转型
do {
parent = t;
// key对象必须实现Comparable接口的compareTo()方法
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
*/
}
}
5.3、Map接口
Map接口(JDK 8)的特点:
-
Map与Collection并列存在,用于保存具有映射关系的数据:Key-Value
-
Map中的Key和Value可以是任意引用类型的数据,会封装到HashMap@Node对象中
-
Map中的key不允许重复,key相同时会进行value替换。原因和HashSet一样,HashSet只是让Value值为常量对象PRESENT。
-
Map中的value可以重复
-
Map的key可以为null,value也可以为null。key为null只能有一个,value为null可以多个
-
常用String类作为Map的key,key的类型可以为任意Object类及其子类。
-
key和value之间存在单向一对一关系,即通过指定的key总能找到对应的value
-
Map存放数据的key-value,一对k-v是放在一个HashMap$Node中的,因为Node实现了Entry接口,也说是一对k-v就是一个Entry
。k-v 是存放在HashMap$Node node = newNode(hash, key, value, null)
k-v为了方便程序员的遍历,还会创建EntrySet集合,该集合存放的元素的类型是Entry
。而一个Entry对象就有k-v属性,即EntrySet<Entry<K, V>>源码声明:
Set<Map.Entry<K, V>> entrySet();
在entrySet的定义类型是Map.Entry,但是实际上存放的还是HashMap$Node,因为HashMap$Node实现了Map.Entry接口(接口的多态 -> 接口的引用可以指向实现接口的类的对象实例),把HashMap$Node存放在entrySet就方便程序员遍历(entrySet中存放的是HashMap$Node的table表的地址),因为Map.Entry提供了抽象方法
K getKey()
、K getValue()
,这样在实现子类Node中可以重写方法,运行时就可以动态绑定到子类Node重写的方法 -> HashMap$Node拥有getKey()和getValue()方法。Map.Entry和HashMap$Node的源码:
interface Entry<K, V> {} // Map的一个子接口 static class Node<K,V> implements Map.Entry<K,V> {} // HashMap的静态内部类
Map常用方法:
- put(Object key, Object value):添加键值对
- remove():根据键删除映射关系
- get(Object obj):根据键获取值,根据传入的key值,返回key对应的value值。
- size():获取元素个数
- isEmpty():判断元素个数是否为0
- clear():清空容器
- containsKey():查找键是否存在
- keySet():获取所有的键
- entrySet():获取所有的key-value
- values():获取所有的值
package com.yxz.map_;
import java.util.HashMap;
import java.util.Map;
public class UseMap {
@SuppressWarnings({"all"})
public static void main(String[] args) {
Map map = new HashMap();
map.put("邓超", new Book("", 100));
map.put("邓超", "孙俪");
map.put("王宝强", "马蓉");
map.put("鹿晗", "关晓彤");
map.put("黄子韬", "徐艺洋");
map.put(null, "刘亦菲");
System.out.println("map = " + map);
// remove:根据键删除映射关系
map.remove(null);
System.out.println("map = " + map);
// get:根据键获取值
Object val = map.get("鹿晗");
System.out.println("val = " + val);
// size:获取元素个数
System.out.println("k-v = " + map.size());
// isEmpty:判断个数是否为0
System.out.println(map.isEmpty());
// clear:清空
// map.clear();
// containsKey:查找键是否存在
System.out.println(map.containsKey("yxz")); // false
}
}
class Book {
private String name;
private int num;
public Book(String name, int num) {
this.name = name;
this.num = num;
}
}
Map的六大遍历方式:
- 通过ketSet()方法,先取出所有的key,再通过key获取到对应value(增强for、迭代器)
- 通过values()方法,取出所有的value(增强for、迭代器)
- 通过entrySet()方法,来获取key-value(增强for、迭代器)
package com.yxz.map_;
import java.util.*;
public class UseMap {
@SuppressWarnings({"all"})
public static void main(String[] args) {
Map map = new HashMap();
map.put("邓超", "123");
map.put("邓超", "孙俪");
map.put("王宝强", "马蓉");
map.put("鹿晗", "关晓彤");
map.put("黄子韬", "徐艺洋");
map.put(null, "刘亦菲");
// 第一组:先取出所有的Key,通过Key取出对应的Value
Set keySet = map.keySet();
// 1.增强for
System.out.println("-------第一种方式---------");
for (Object key : keySet) {
System.out.println(key + "-" + map.get(key));
}
// 2.迭代器
System.out.println("-------第二种方式---------");
Iterator iterator = keySet.iterator();
while (iterator.hasNext()) {
Object key = iterator.next();
System.out.println(key + "-" + map.get(key));
}
// 第二组:把所有的values取出来
Collection values = map.values();
// 可以使用所有的Collection使用的遍历方法
System.out.println("-------取出所有的value值 增强for--------");
// 1.增强for
for (Object value : values) {
System.out.println(value);
}
// 2.迭代器
System.out.println("-------取出所有的value值 迭代器--------");
Iterator iterator2 = values.iterator();
while (iterator2.hasNext()) {
Object value = iterator2.next();
System.out.println(value);
}
// 第三组:通过EntrySet来获取k-v
Set entrySet = map.entrySet(); // EntrySet<Map.Entry<K, V>>
// 1.增强for
System.out.println("-----使用EntrySet的增强for(第3种)----------");
for (Object entry : entrySet) {
// 将entry转成Map.Entry -> 实现Map.Entry的子类HashMap$Node就能调用重写Map.Entry的方法getKey,getValue
Map.Entry m = (Map.Entry) entry;
System.out.println(m.getKey() + "-" + m.getValue());
}
// 2.迭代器
System.out.println("-----使用EntrySet的迭代器(第4种)-----------");
Iterator iterator3 = entrySet.iterator();
while (iterator3.hasNext()) {
Object entry = iterator3.next(); // 向上转型
// System.out.println(entry.getClass()); // 动态绑定到实现Map.Entry的接口子类HashMap$Node
// 向下转型 -> Map.Entry
Map.Entry m = (Map.Entry) entry;
System.out.println(m.getKey() + "-" + m.getValue());
}
}
}
总结:
- Map接口的常用实现类:HashMap、Hashtable、Properties
- HashMap是Map接口使用频率最高的实现类
- HashMap是以key-value键值对的方式来存储数据
- key不能重复,但是value可以重复,允许使用null键和null值
如果添加相同的key,则会覆盖原来的key-value
,等同于修改(key不会替换,value值会被替换)- 与HashSet一样,
HashMap不保证映射的顺序
,因为底层是以hash表的方式存储的(JDK 8的HashMap底层 = 数组+链表+红黑树
) - HashMap没有实现同步,线程不安全,方法没有做同步互斥的操作 -> 没有
synchronized
关键字
5.3.1、HashMap类
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {}
HashMap底层存储原理(⭐⭐⭐⭐⭐⭐)
HashMap底层机制及源码分析:
扩容机制(和HashSet相同)
- HashMap底层维护了Node类型的数组table,默认为null
- 当创建对象时,将加载因子(loadfactor)初始化为0.75
- 当添加key-value时,
通过key的hash值得到在table表的索引
。然后判断该索引处是否有元素,如果没有元素直接添加。如果该索引处有元素,继续判断该元素的key是否和准备加入的元素的key相等
,如果相等,则直接替换value
;如果不相等需要判断是树结构还是链表结构并作出相应处理。如果添加元素时发现容量不够则需要扩容。 - 第1次添加,需要扩容table容量为16,临界值(threshold)为12
- 以后再扩容,则需要扩容table容量为原来的2倍,临界值为原来的2倍即24,依此类推...
- 再Java 8中,如果一条链表的元素个数超过TREEIFY_THRESHOLD(默认=8),并且table的大小 >= MIN_TREEIFY_CAPACITY(默认=64),就会对该链表进行树化(红黑树)
package com.yxz.map_;
import java.util.HashMap;
public class HashMapSource {
@SuppressWarnings({"all"})
public static void main(String[] args) {
HashMap map = new HashMap();
map.put("java", 10);
map.put("php", 10);
map.put("java", 20);
System.out.println("map = " + map);
/* 分析源码:
1.执行构造器 new HashMap()
初始化加载因子 loadfactor = 0.75
HashMap$Node[] table = null
2.执行put(),会调用hash()方法,计算key的hash值:return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)
public V put(K key, V value) { // key = "java", value = 10
return putVal(hash(key), key, value, false, true);
}
3.执行putVal()
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i; // 辅助变量
// 如果底层的table 数组为null,或者length = 0,就扩容到16
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 取出hash值对应的table的索引位置的Node,如果为null,就直接把加入的k-v
// 创建成一个Node,加入该位置即可
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash && // 如果table的索引位置的key的hash和新的key的hash值相同,并且满足两个条件之一的:
// 条件(1)table现有的节点的key和准备添加的key是同一个对象(==判断同一个对象)
// 条件(2)equals()返回真
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode) // 如果当前table已有的Node是红黑树,就按照红黑树的方式处理
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 如果找到的节点后面是链表,就循环比较该链表上的每一个节点
for (int binCount = 0; ; ++binCount) { // 死循环,只有通过两种条件的break才能跳出
if ((e = p.next) == null) { // 第1个条件:整个链表遍历完都没有相同的节点
p.next = newNode(hash, key, value, null); // 链表尾加上该节点
// 加入新节点后,判断当前链表的节点个数,满8个就树化
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash); // 树化
break; // 退出循环
}
if (e.hash == hash && // 第2个条件:循环比较中,有一个节点和准备加入的节点相同,就只是替换相同key的value值
((k = e.key) == key || (key != null && key.equals(k))))
break; // 什么都不做,退出循环
p = e; // 更新p指针,保持和e指向统一
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value; // 替换key对应的value值
afterNodeAccess(e);
return oldValue;
}
}
++modCount; // 每增加一个Node,就size++
if (++size > threshold) // 如果size > 临界值,就扩容
resize();
afterNodeInsertion(evict);
return null;
}
5.关于树化(转成红黑树)
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// 如果table为null,或者大小还没有到64,暂时不树化,只扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
// table容量到64,且有一个链表节点数满8个 -> 链表树化
else if ((e = tab[index = (n - 1) & hash]) != null)
*/
}
}
5.3.2、Hashtable类
public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable {}
说明:
-
存放的元素是键值对key-value
-
Hashtable的键和值都不能为null
,否则会抛出异常NullPointerException -
Hashtable使用方法基本上和HashMap一样
-
Hashtable是线程安全的(synchronized)
,HashMap线程不安全 -
Hashtable底层有数组Entry[],初始化大小为11,Entry[]数组是Hashtabel的静态内部类Hashtable$Entry类型
临界值threshold = 11 * 0.75 = 8
扩容机制:当满足
(count >= threshold)
时,就按照int newCapacity = (oldCapacity << 1) + 1;
的大小进行扩容。(新容量 = 原容量 * 2 + 1)执行方法
addEntry(hash, key, value, index);
将K-V封装成Entry并添加
HashMap和Hashtable对比
支持版本 | 线程安全(同步) | 效率 | 允许null键null值 | |
---|---|---|---|---|
HashMap | 1.2 | 不安全 | 高 | 可以 |
Hashtable | 1.0 | 安全 | 较低 | 不可以 |
5.3.3、Properties类
public class Properties extends Hashtable<Object,Object> {}
说明:
Properties类继承自Hashtable类并且实现了Map接口
,也是使用一种键值对的形式来保存数据
(key和value也不能为null,否则抛出空指针异常)。- 它的使用特点和Hashtable类似
- Properties还可以用于从
xxx.properties文件
中,加载数据到Properties类对象,并进行读取和修改 - 开发中,
xxx.properties文件
通常作为配置文件,在IO流章节详解 - Properties的
getProperty(String key)方法要求key值必须为String类型,get()方法的key值可以为任意类型
5.3.4、TreeMap类
public class TreeMap<K,V>
extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable
说明:
- 类似于TreeSet类,这里TreeMap是双列存储,以key-value形式存储。
- 如果当前要加入的key和一个已经在TreeMap中的key值一样,则添加失败,但是会替换原有value值。
- TreeSet底层就是TreeMap,执行时源码一致,只是value值是变量。
- key不能为null,value可以为null。
开发中如何选择集合实现类
选择什么集合实现类,主要取决于业务操作特点
,根据集合实现类特点进行选择
-
先判断存储的类型(一组对象[单列]或一组键值对[双列])
-
一组对象[单列]:Collection接口
-
允许重复:List
增删操作多:LinkedList(底层 = 双向链表)
改查操作多:ArrayList(底层 = Object类型的可变数组)
-
不允许重复:Set
无序:HashSet(底层 = HashMap = 哈希表 = 数组 + 链表 + 红黑树)
排序:TreeSet
插入和取出顺序一致:LinkedHashSet(底层是LinkedHashMap, 底层 = 数组 + 双向链表)
-
-
一组键值对[双列]:Map
- 键无序:HashMap(底层 = 哈希表, JDK 7: 数组 + 链表, JDK 8: 数组 + 双链表 + 红黑树)
- 键排序:TreeMap
- 键插入和取出顺序一致:LinkedHashMap(底层是HashMap)
- 读取文件:Properties
面试题:HashSet和TreeSet分别如何实现去重的?
- HashSet去重机制:hashCode()+equals(). 底层先通过存入key对象, 进行运算得到一个hash值, 通过hash值得到对应的索引,如果发现table索引所在的位置没有数据则直接存放;
如果有数据, 则进行==或equals()比较(如果索引处是单链表则循环遍历比较)
, 比较后不相同就在表尾处加入, 否则不加入. - TreeSet去重机制:如果传入了一个Comparator比较器(匿名内部类, 匿名对象), 就使用实现的compare()方法去重, 如果方法返回0就认为是相同元素(对象), 就不添加. 如果没有传入比较器, 则以添加的对象实现的Comparable接口的compareTo去重.(如果对象所属类型没有实现Comparable接口则抛出类型转换异常)
5.4、Collections
Collections工具类
- Collections是一个操作Set、List和Map等集合的工具类
- Colllections中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作。
排序操作(均为static方法)
- reverse(List):反转List中元素的顺序
- shuffle(List):对List集合元素进行随机排序
- sort(List):根据元素的自然顺序对指定List集合元素按升序排序
- sort(List, Comparator):根据指定的Comparator产生的顺序对List集合元素进行排序
- swap(List, int, int):将指定List集合中的i处元素和j处元素进行交换
查找、替换操作
- Object max(Collection):根据元素的自然顺序, 返回给定集合中的最大元素
- Object max(Collection, Comparator):根据Comparator指定的顺序, 返回给定集合中的最大元素
- Object min(Collection):
- Object min(Collection, Comparator):
- int frequency(Collection, Object):返回指定集合中指定元素的出现次数
- void copy(List dest, List src):将src的内容复制到dest中
- boolean replaceAll(List list, Object oldVal, Object newVal):使用新值替换List对象的所有旧值
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class Test {
@SuppressWarnings({"all"})
public static void main(String[] args) {
List list = new ArrayList();
list.add("tom");
list.add("smith");
list.add("king");
list.add("micai");
list.add("queen");
list.add("milan");
list.add("micai");
list.add("tom");
// 1. reverse(List):反转List中元素的顺序
Collections.reverse(list);
System.out.println("反转后的\nlist = " + list);
// 2. shuffle(List):对List集合元素进行随机排序
Collections.shuffle(list);
System.out.println("随机排序后的\nlist = " + list);
// 3. sort(List):根据元素的自然顺序对指定List集合元素按升序排序
Collections.sort(list);
System.out.println("自然顺序升序排序后的\nlist = " + list);
// 4. sort(List, Comparator):根据指定的Comparator产生的顺序对List集合元素进行排序
Collections.sort(list, new Comparator() {
@Override
public int compare(Object o1, Object o2) {
return ((String) o2).compareTo((String) o1); // 字典序降序排序
}
});
System.out.println("字典序降序后的\nlist = " + list);
// 5. swap(List, int, int):将指定List集合中的i处元素和j处元素进行交换
Collections.swap(list, 0, 1);
System.out.println("交换后的\nlist = " + list);
// 1. Object max(Collection):根据元素的自然顺序, 返回给定集合中的最大元素
System.out.println("自然顺序最大的元素 = " + Collections.max(list));
// 2. Object max(Collection, Comparator):根据Comparator指定的顺序, 返回给定集合中的最大元素
Object maxObj = Collections.max(list, new Comparator() {
@Override
public int compare(Object o1, Object o2) {
return ((String) o1).length() - ((String) o2).length(); // 按字符串长度从小到大排序
}
});
System.out.println("长度最大的元素 = " + maxObj);
// 3. Object min(Collection):
// 4. Object min(Collection, Comparator):
// 5. int frequency(Collection, Object):返回指定集合中指定元素的出现次数
System.out.println("micai出现的次数 = " + Collections.frequency(list, "micai"));
// 6. void copy(List dest, List src):将src的内容复制到dest中
// 为了完成一个完整的拷贝, 需要先给dest赋值, 大小和list.size()一样
ArrayList dest = new ArrayList();
for (int i = 0; i < list.size(); i++) {
dest.add("");
}
Collections.copy(dest, list);
System.out.println("拷贝后的\ndest = " + dest);
// 7. boolean replaceAll(List list, Object oldVal, Object newVal):使用新值替换List对象的所有旧值
Collections.replaceAll(list, "tom", "汤姆");
System.out.println("替换tom后的\nlist = " + list);
}
}
6、泛型
泛型(generic)的好处:
- 编译时, 编译器会检查添加元素的类型, 提高了安全性.
- 减少了类型转换的次数, 提高了效率.
- 不再提示编译警告.
泛型介绍:泛(广泛)型(数据类型)
泛型又称参数化类型, 即接收数据类型的类型
, 是JDK 5.0出现的新特性, 解决数据类型的安全性问题.- 在类声明或实例化时, 只要指定好需要的具体的类型即可.(类似于模板)
- Java泛型可以保证如果程序在编译时没有发出警告, 运行时就不会产生ClassCastException异常. 同时, 代码更加简洁, 健壮.
- 泛型的作用:可以在类声明时通过一个标识表示类中某个属性的类型, 或者是某个方法的返回值的类型, 或者是参数类型.
- 给泛型指定具体的泛型类型后, 只能存放指定类型的对象而不会向上转型, 从容器中取出对象时也是指定类型, 不再需要向下转型.
package com.yxz.generic;
import java.util.ArrayList;
@SuppressWarnings({"all"})
public class Generic02 {
public static void main(String[] args) {
// 传统方法 ==> 使用泛型
// 1.当使用ArrayList<Dog> 表示存放到ArrayList集合中的元素是Dog类型
// 2.如果编译器发现添加的类型不满足要求,就会报错(数据类型约束)
// 3.遍历的时候,可以直接取出 Dog 类型而不是 Object
// ArrayList arrayList = new ArrayList();
ArrayList<Dog> arrayList = new ArrayList<Dog>();
arrayList.add(new Dog("旺财", 10));
arrayList.add(new Dog("发财", 5));
arrayList.add(new Dog("大黄", 3)); // 放入时就是Dog类型, 而不会向上转型成Object
// 不小心,添加了一只猫
// arrayList.add(new Cat("招财猫", 4));
// for (Object o : arrayList) {
// Dog o1 = (Dog) o;
// System.out.println(o1.getName() + ":" + o1.getAge() + "岁");
// }
for (Dog dog : arrayList) { // 取出时就是Dog, 无需再向下转型
System.out.println(dog.getName() + ":" + dog.getAge() + "岁");
}
}
}
class Dog {
private String name;
private int age;
public Dog(String name, int age) {
this.name = name;
this.age = 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;
}
}
class Cat {
private String name;
private int age;
public Cat(String name, int age) {
this.name = name;
this.age = 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;
}
}
6.1、泛型语法
泛型的声明:
interface 接口<T>{}
和 class 类名<K, V, S>{}
说明:
- 其中T,K,V,S不代表值, 而是表示类型
- 任意字母表示都可以(也可以使用其他标识符),
常用T表示
, 是Type的缩写.一般用单个大小字母表示泛型类型
泛型的实例化:要在类名后面指定类型参数的值(类型).
如:List
注意:
泛型的类型只能是引用类型
, 不能是基本数据类型.- 在给泛型指定泛型具体类型后, 可以传入该类型或者其子类类型.
- 泛型使用形式:
- List
list1 = new ArrayList (); List<Integer> list2 = new ArrayList<>(); -> 编译器会进行类型推断(推荐使用)
- List
- 如果写成
List list1 = new ArrayList();
-> 表示默认给它的泛型是Object类型 --等价于-> List