1. 计算机常识
1.1 常用的DOS
命令
- 复制、粘贴:选中内容右键就是复制、不选中内容右键就是粘贴。
dir
:列出当前路径下所有文件、目录。cls
:清屏。exit
:关闭DOS
窗口。c:
、d:
:切换盘符。del
:del fileName.txt
删除指定文件、del *.txt
删除所有以.txt
结尾的文件。ipconfig
:查看电脑网卡信息,如IP
等。ipconfig/all
查看网卡的详细信息,如Mac
地址。ping ip
:查看两台计算机之间是否可以正常通信。mkdir directoryName
:新建目录。rd directoryName
:删除目录。ren oldName newName
:修改目录名。cd
:cd /
进入当前盘符的根路径、cd 相对路径/绝对路径
进入指定目录、cd ..
回到上级目录。type fileName
:查看指定文件的内容。shutdown -s -t 3600
:3600s
之后电脑关机。
1.2 .bat
文件
.bat
文件即批处理文件,可以批量执行DOS
命令。下面是一个简单的示例:
@echo off
REM 这是一条注释
echo Hello World!
pause
.bat
脚本一般用于系统维护与自动化,有以下常见应用场景:
-
定时清理临时文件:删除系统垃圾文件或日志,释放磁盘空间。
@echo off del /q /f %TEMP%\*.* del /q /f C:\Windows\Temp\*.* echo 临时文件已清理! pause
-
自动备份重要数据:复制指定文件夹到备份目录。
@echo off robocopy "C:\Documents" "D:\Backup\Documents" /MIR echo 备份完成!
-
软件批量安装
@echo off start /wait installer_chrome.exe /silent /install start /wait installer_7zip.exe /S echo 软件安装完成!
-
配置开发环境
@echo off setx PATH "%PATH%;C:\Python310" pip install requests numpy
1.3 计算机中的存储容量单位
在计算机中,内存大小通常使用二进制单位来表示,这些单位包括位bit
、字节Byte
、千字节KB
、兆字节MB
、吉字节GB
、太字节TB
等。
-
位
bit
:这是计算机中最小的数据单位,它只有两个状态,即0
和1
。位是计算机内部数据处理的基础。 -
字节
Byte
:字节由8
个位组成,是计算机中用来表示内存储器容量大小的基本单位。 -
千字节
KB
:等于1024
个字节。在计算机中,KB
通常用来表示较小的数据大小。 -
兆字节
MB
:等于1024
个KB
,即1024*1024
个字节。在存储容量方面,MB
是一个较大的单位,通常用于表示中等规模的数据大小。 -
吉字节
GB
:等于1024
个MB
,即1024*1024*1024
个字节。GB
是一个非常大的单位,通常用于表示大规模的数据存储。 -
太字节
TB
:等于1024
个GB
,即1024*1024*1024*1024
个字节。TB
是一个巨大的单位,通常用于表示非常大的数据存储,如硬盘、数据中心等。
除了上述常见的单位外,还有一些更大的单位,如PB
、EB
、ZB
和BB
等,这些单位通常用于表示更大规模的数据存储和计算。
1.4 字符编码
字符编码是人为规定的文字与二进制之间的转换关系。在早期的计算机系统中,字符编码主要采用ASCII
编码,采用1
个字节编码,最多可以表示256
个字符,常用的ASCII
码如下:
a-z
:97-122
A-Z
:65-90
0-9
:48-57
编码中的基本概念:
- 编码与解码:编码是将字符转化为二进制数据的过程,解码反之。如果编码和解码使用的不是同一套字符集,就会出现乱码的问题。
- 字符集:是多个字符的集合,每个字符在字符集中都有一个唯一的编号,即码点。
- 码点
code point
:指一个字符集中的某个字符对应的代码值。在Unicode
中,码点采用十六进制书写,如U+1002
。
常见的字符编码如下:
ASCII
:采用1
个字节编码,包括字母、数字、符号、控制字符等。Unicode
:万国码,可表示所有语言的字符集,占2
个字节或4
个字节,但是使用这种方式是比较浪费空间的。UTF-8
:基于Unicode
编码的可变长度字符编码,使用1~4
个字节表示一个字符,是目前最常用的字符编码方式,可以避免空间浪费。GBK
:中国国家标准的简体中文字符集,使用2
个字节表示一个汉字。
2. Java
语言概述
Java
是1995
年Sun
公司推出的一门面向对象
的高级编程语言,底层是由C++
实现,Java
之父是James Gosling
。sun
公司于2010年1月
被Oracle
收购。
2.1 JDK
版本变化
在 JDk1.7
升级到JDK8
的过程中,oracle
公司弃用了原本的1.x
版本号名称。但是JDK8
在使用java -version
查询版本时依然是1.8.x
,这一问题一直到JDK 9
才彻底解决。下面是几个比较重要的Java
版本:
JDK8
添加了lambda
表达式、JavaFX
、流式处理
以及JS
脚本引擎等功能。JDK9
在JDK8
基础上添加了许多新类,优化了线程并发处理和垃圾回收处理的代码,并开启了模块化Java API
的先河,不过很快被JDK10
取代。JDK10
添加了var
声明局部变量,同时进一步优化了JDK9
的代码,并删除了冗余的过时代码。JDK11
优化了垃圾处理的机制,删除了Nashorn JavaScript
引擎、JavaFX
,JavaFX
分离为一个独立的框架。JDK 11
是一个长期更新版本。JDK17
作为继JDK11
后的长期支持版本,引入密封类增强安全性,优化垃圾回收提升性能,提高平台兼容性,移除过时特性。JDK17
是未来的主流版本。
2.2 Java
的三大分支
Java
分为三个分支,主要是JavaSE
和JavaEE
,JavaME
已经逐渐被取代。
JavaSE
:是Java
的标准版,包含了Java
语言的核心部分,包括基础类库、虚拟机和开发工具等。JavaSE
主要用于开发桌面应用程序、控制台程序和小型服务器端应用程序等。JavaEE
:是Java
的企业版,它在JavaSE
的基础上扩展而来,主要用于开发大型企业级应用程序,如电子商务系统、EPR
系统和CRM
系统等。JavaEE
包含了许多企业级技术,如Servlet
、JSP
、EJB
、JTA
等。JavaME
:是Java
的微型版,它主要用于嵌入式设备和移动设备上的应用程序开发,如手机、平板电脑、数码相机等等。JavaME
的特点就是体积小、速度快、资源占用少,可以在较小的内存和处理能力的设备上运行。
2.3 Java
语言的特点
- 简单易学:
Java
是c++
的纯净版本。 - 面向对象:支持封装、继承和多态。
- 平台无关性:即跨平台性,一次编译到处运行。
- 平台:指操作系统,如
Windows、Mac、Linux
。 - 跨平台性:
Java
程序可以在任意操作系统上运行。 JVM
:Java
虚拟机,Java Virtual Machine
,本质上也是一款应用软件。- 实现原理:在不同的操作系统中,都安装了一个与操作系统对应的
Java
虚拟机,Java
虚拟机作为Java
语言与操作系统之间的桥梁,屏蔽了各个操作系统之间的差异,使得Java
语言具有了跨平台性。 - 安全性
- 高性能
- 多线程
- 自动垃圾回收机制
- 平台:指操作系统,如
2.4 搭建Java
运行环境
要搭建Java
的运行环境,只需要安装JDK
和配置path
环境变量即可。
2.4.1 安装JDK
使用JDK
开发Java
程序,使用JRE
运行Java
程序:
JDK【Java Development Kit】
:Java
程序开发工具包,包含了JRE
和开发人员使用的工具。两个重要目录:bin
目录:包含了Javac.exe
和Java.exe
。src.zip
:用于存放核心类库的源码。
JRE【Java Runtime Environment】
:Java
运行环境,包含了JVM
和Java
程序所需要的核心类库。如果只是想要运行一个开发好的Java
程序,计算机只需要安装JRE
即可。
2.4.2 配置path
环境变量
【Path
环境变量】:配置path
环境变量的主要作用就是告诉操作系统在哪里查找可执行文件,如命令、脚本或程序。
那为什么需要配置环境变量呢?在DOS
中启动某程序时,需要进入到该程序所在目录才可以启动该程序,否则会报错。我们希望在任意路径下都能使用Javac.exe
、Java.exe
等程序,因此需要配置path
环境变量。配置Path
环境变量后,在DOS
中启动某程序时,系统会根据以下规则寻找并打开该程序:
- 系统首先会在当前目录下搜索该程序并启动,如果该程序在当前目录下,则启动该程序。
- 如果当前目录下没有该程序,则系统会进入到环境变量中搜索该程序所在目录,如果
path
条目中添加了该程序的所在目录,则启动该程序。 - 如果
path
中没有添加该程序的所在目录,则启动该程序时会报错。
【配置方法】:在系统环境变量中添加JAVA_HOME
条目,值为JDK
所在的文件目录,然后再在path
条目中添加%JAVA_HOME%\bin
即可。如果配置成功,在命令行中输入java -version
会输出对应的Java
版本。
2.4.3 在cmd
中运行Java
程序
运行已编译的程序时,Java
虚拟机总是从指定类中的main
方法的代码开始执行,因此为了代码能够执行,在类的源文件中必须包含一个main
方法。main
方法必须声明为public static void
。
- 用记事本编写一个名为
HelloWorld.txt
的java
程序,并将文件后缀改为.java
。 - 进入该
java
程序所在文件目录。 - 运行
javac HelloWorld.java
生成字节码文件HelloWorld.class
。 - 运行
java HelloWorld
即可。
2.4.4 Java
的加载与执行
Java
程序的加载与执行是一个从源代码到运行结果的多阶段过程,涉及编译阶段、运行阶段。
- 编译阶段:使用
javac
将.java
文件转换为.class
字节码文件。 - 运行阶段:
- 类加载阶段:
JVM
通过类加载器将.class
文件加载到内存。- 加载:查找
.class
文件并读入二进制数据。 - 链接:分为验证【检查字节码是否符合规范】、准备【为静态变量分配内存并赋默认值】和解析【将符号引用转换为直接引用】。
- 初始化:执行静态代码块和静态变量赋值。
- 加载:查找
- 执行阶段
- 解释器:逐行解释执行字节码。
JIT
编译器:将热点代码编译为本地机器码。GC
:自动管理内存,回收无用对象。
- 类加载阶段:
对于Java
的加载与执行,有以下注意点:
- 包含两个阶段,编译与运行,编译和运行可以在不同的操作系统上完成。
- 编译后删除
.java
文件对程序不会有任何影响。 .class
文件是字节码文件,而非机器码,操作系统无法直接执行,但是JVM
可以看懂,可以把字节码解释为机器码。
2.4.5 CLASSPATH
CLASSPATH
是 Java
环境中用于指定类文件.class
和依赖库.jar
搜索路径的配置项,直接影响 Java
编译器javac
和虚拟机JVM
查找用户自定义类及第三方库的位置,说白了CLASSPATH
就是给类加载器指路的。
如果CLASSPATH
没有配置的话,默认从当前路径下找.class
字节码文件。如果配置了CLASSPATH
,那么类加载器只会从CLASSPATH
中查找字节码文件了,不会再从当前路径下寻找。如果想要既从当前路径下查找,又从CLASSPATH
中查找,则可以按如下配置:
// . 表示当前路径
.;C:\myproject\lib
3. Java
基础语法
3.1 Java
注释
注释的内容不参与编译,编译后的字节码文件.class
中不包含注释内容。Java
注释用于说明和解释代码,帮助开发人员快速解读代码。注释并不是越多越好,在合适的位置上添加注释即可。
-
单行注释:
// 注释内容
-
多行注释:
/* 注释内容 */
-
文档注释:
/** 注释内容 */
,文档注释可以被JDK
提供的Javadoc
工具解析,生成网页文件形式的说明文档,如javadoc -d docs -author -version -encoding utf-8 HelloWorld.java
。package com.person; /** * 文档注释 * @author Bruce * @version 1.0 */ public class DruidConfig { public static void main(String[] args) { // main方法 /* 这是一个简单的java程序, 功能如下: 输出Hello World */ System.out.println("Hello world"); } }
3.2 标识符
在Java
中,标识符是用来给变量、方法、类和包等命名的字符序列。
标识符的命名规则如下:必须遵循的规则
- 标识符由字母、数字、
_
、$
组成,且不能以数字开头。这里的字母指任何一个国家的文字,包括中文。 - 标识符不能是
java
中的关键字,如public
、void
等。 - 标识符严格区分大小写。
- 标识符的长度没有限制。
标识符的命名规范如下:约定俗成的规范
- 标识符应该简短,且见名知意,一般使用驼峰命名法。
- 类名、接口名、枚举、注解:首字母大写,后面每个单词首字母大小,如
MyClass
。 - 变量名和方法名:首字母小写,后面每个单词的首字母大写,如
userCount
。 - 常量名:全部大写,每个单词之间用
_
连接,如MAX_VALUE
。 - 包名:全部小写,单词之间使用
.
分隔,且包名一般采用域名反转的形式。
3.3 关键字
关键字是被Java
语言赋予了特殊含义,用作专门用途的单词。
-
所有关键字、保留字
(Java现有版本中没有特殊含义)
都由小写字母组成,且关键字和保留字不可以作为标识符来使用。 -
关键字共有
50
个,其中保留字有const
和goto
。Java
关键字类别Java
关键字关键字含义 访问控制 private
一种访问控制方式:私有模式,访问控制修饰符,可以应用于类、方法或字段 访问控制 protected
一种访问控制方式:受保护模式,可以应用于类、方法或字段的访问控制修饰符 访问控制 public
一种访问控制方式:公用模式,可以应用于类、方法或字段的访问控制修饰符。 类、方法和变量修饰符 abstract
表明类或者成员方法具有抽象属性,用于修改类或方法 类、方法和变量修饰符 class
声明一个类,用来声明新的 Java
类类、方法和变量修饰符 extends
表明一个类型是另一个类型的子类型 类、方法和变量修饰符 final
用来说明最终属性,表明一个类不能派生出子类,或者成员方法不能被覆盖,或者成员域的值不能被改变,用来定义常量 类、方法和变量修饰符 implements
表明一个类实现了给定的接口 类、方法和变量修饰符 interface
接口 类、方法和变量修饰符 native
用来声明一个方法是由与计算机相关的语言如 C/C++/FORTRAN
语言实现的类、方法和变量修饰符 new
用来创建新实例对象 类、方法和变量修饰符 static
表明具有静态属性 类、方法和变量修饰符 strictfp
用来声明 FP_strict
【单精度或双精度浮点数】表达式遵循IEEE 754
规范类、方法和变量修饰符 synchronized
表明一段代码需要同步执行 类、方法和变量修饰符 transient
声明不用序列化的成员域 类、方法和变量修饰符 volatile
表明两个或者多个变量必须同步地发生变化 程序控制 break
提前跳出一个块 程序控制 continue
回到一个块的开始处 程序控制 return
从成员方法中返回数据 程序控制 do
用在 do-while
循环结构中程序控制 while
用在循环结构中 程序控制 if
条件语句的引导词 程序控制 else
用在条件语句中,表明当条件不成立时的分支 程序控制 for
一种循环结构的引导词 程序控制 instanceof
用来测试一个对象是否是指定类型的实例对象 程序控制 switch
分支语句结构的引导词 程序控制 case
用在switch语句之中,表示其中的一个分支 程序控制 default
默认,例如用在 switch
语句中,表明一个默认的分支。Java8
中也作用于声明接口函数的默认实现错误处理 try
尝试一个可能抛出异常的程序块 错误处理 catch
用在异常处理中,用来捕捉异常 错误处理 throw
抛出一个异常 错误处理 throws
声明在当前定义的成员方法中所有需要抛出的异常 包相关 import
表明要访问指定的类或包 包相关 package
包 基本类型 boolean
基本数据类型之一,声明布尔类型的关键字 基本类型 byte
基本数据类型之一,字节类型 基本类型 char
基本数据类型之一,字符类型 基本类型 double
基本数据类型之一,双精度浮点数类型 基本类型 float
基本数据类型之一,单精度浮点数类型 基本类型 int
基本数据类型之一,整数类型 基本类型 long
基本数据类型之一,长整数类型 基本类型 short
基本数据类型之一,短整数类型 基本类型 null
空值 基本类型 true
真 基本类型 false
假 变量引用 super
表明当前对象的父类型的引用或者父类型的构造方法 变量引用 this
指向当前实例对象的引用,用于引用当前实例 变量引用 void
声明当前成员方法没有返回值, void
可以用作方法的返回类型,以指示该方法不返回值保留字 goto
保留关键字,没有具体含义 保留字 const
保留关键字,没有具体含义,是一个类型修饰符,使用 const
声明的对象不能更新
3.4 字面量
字面量指的是在程序中直接使用的数据,字面量是Java
语言中最基本的表达式,不需要进行计算或转换,直接使用即可。
- 整数型:
12
、-5
- 浮点型:
3.44
、-2.5
- 布尔型:
false
、true
- 字符串型:
"你好世界"
- 字符型:
'a'
、'.'
null
:null
3.5 变量Variable
3.5.1 变量的声明与赋值
变量就是内存中的一块空间,是计算机中存储数据的最基本的单元。变量的三要素如下:
- 数据类型:决定空间的大小。
- 变量名
- 变量值:变量中具体的存储数据。
变量的声明、赋值、初始化如下:
-
声明:声明变量就是告诉编译器这个变量的数据类型,以便分配内存大小。从
Java10
开始,会与局部变量,可以使用var
关键字声明变量来自动推断变量类型。 -
赋值:为已声明的变量赋予新的值。
-
初始化:变量声明后的第一次赋值称为初始化。
// 初始化 a int a = 20; // 声明 b int b; b = 30; // 初始化 b b = 99; // 赋值 int c = 1, d = 2; // 不推荐 int e = 30; int f = 50; // var var name = "Bruce";
变量的作用其实很简单,就是【便于代码维护】和【增强代码可读性】。在使用变量时,有以下注意点:
- 变量必须先声明,再赋值,然后才能访问。
- 一行代码上可以同时声明多个变量。
- 在同一作用域上,变量不能重名,但是可以重新赋值。
- 变量的字面值类型必须和变量的类型一致,否则代码会报错。
3.5.2 变量的分类
变量可以根据数据类型和作用域来进行分类,如下:
- 按数据类型分类:基本数据类型、引用数据类型
- 按作用域分类
- 局部变量:声明在方法体、代码块内部的变量,包括方法的形参。
- 成员变量:成员变量是定义在类中的变量,也叫做属性,又分为实例变量、静态变量。
- 实例变量:类中声明的非静态变量。
- 静态变量:类中声明的静态变量【用
static
修饰的实例变量】,也可以称为类变量。
实例变量和静态变量的主要区别就在于声明周期:
- 实例变量:随对象的创建而创建,随对象的销毁而销毁,每个对象都有自己的副本,实例变量只能通过对象访问。
- 静态变量:随着类加载时创建,随着类卸载时而销毁,无论创建多少个对象,都只有一个类变量的副本,类变量通过类访问。
局部变量和成员变量还有以下区别:
-
初始化:局部变量必须显式地进行初始化,而全局变量会默认进行初始化,如下:
short、byte、int:0 long:0L float:0.0f double:0.0 char:'0' boolean:false 引用数据类型:null
-
修饰符:局部变量不能使用修饰符,而全局变量可以使用修饰符。
3.5.3 变量的作用域
变量的作用域就是变量的有效范围,由声明变量的位置决定,不同的作用域决定了变量的声明周期和可见性。在java
中,变量的作用域主要分为以下四种:
作用域类型 | 声明位置 | 生命周期 | 作用范围 |
---|---|---|---|
类作用域【实例变量】 | 类的内部,方法外部 | 随对象创建或销毁 | 整个类内部 |
包作用域【静态变量】 | 类的内部,方法外部 + static | 类加载到程序结束 | 该类所在的整个包 |
方法作用域【局部变量】 | 方法内部或参数列表 | 方法调用开始到结束 | 方法内部 |
块作用域【块内变量】 | 代码块内部,如 {} 、循环、条件语句 | 代码块执行期间 | 代码块内部 |
对于作用域,有以下规则和注意事项:
- 就近访问原则:如果局部变量与成员变量同名,优先访问局部变量。
- 作用域覆盖冲突:同一作用域内不能重复声明同名变量。
- 跨作用域访问限制:外层作用域无法访问内层作用域的变量。
作用域的不同,内存管理方式也是有区别的,如下:
- 成员变量:存储在堆内存中,声明周期较长。
- 局部变量:存储在栈内存中,随方法调用入栈,方法结束出栈销毁。
- 静态变量:存储在方法区,声明周期最长。
3.6 基本数据类型
Java
是一种强类型语言
,这就意味着必须为每一个变量声明类型,且不能改变。Java
中有8
种基本数据类型来存储数值、布尔值以及字符。
当基本数据类型用作全局变量时,默认值如下:
byte、short、int: 0
long: 0L
float: 0.0F
double: 0.0
boolean: false
char: '0'
3.6.1 整数类型
整数类型简称【整型】,用来存储整数数值。整型数据根据它所占内存大小的不同,可分为byte
、short
、int
、long
4种类型。int
型最常用,byte
和short
类型主要用于特定的应用场合,例如底层的文件处理或者存储空间很宝贵时的大数组。
类型 | 内存空间 | 取值范围 |
---|---|---|
byte | 1 byte | -128 ~ 127 |
short | 2 byte | -32768 ~ 32767 |
int | 4 byte | -2^31 ~ 2^31 - 1 ----> 21 亿左右 |
long | 8 byte | -2^63 ~ 2^63 - 1 |
int
是Java
整数类型的默认数据类型,也就是说,Java
中的任何一个整数型字面量都会被当作int
类型类处理。如果在整数型字面量后加上L
或者l
,那么这个字面量就会被当作long
类型来处理。
Java
中允许小容量的数据直接赋值给大容量的变量,即【自动类型转换】,规则为byte -> short -> int -> long
。
// 100 会被当作int处理,所以不存在自动类型转换
int number = 100;
// 100 会被当作int处理, 但是符合自动类型转换
int numberLong = 100;
// 100 会被当做long处理
int numberLong = 100L;
// 程序会报错, 因为 100000000000000000 会被当做int处理,但是该数值超出int能够存储的数据的范围,发生内存溢出
long numberLong = 100000000000000000;
// 不会报错
long numberLong = 100000000000000000L;
Java
中的大容量是无法直接转换成小容量的。因为这种操作可能会导致精度损失,如果想要实现这种操作,可以使用【强制类型转换】,这样程序编译才能通过。
// 不存在精度损失
long numberLong = 55L;
int numberInt = (int)numberLong;
// 存在精度损失
int numberInt = 128;
byte numberByte = (byte)numberIne; // -128
【优化】在Java
中,当整数字面值没有超过byte
、short
的范围时,直接将其赋值给byte
或short
时,程序是不会报错的。
byte number = 127; // 不会报错
byte number = 128; // 编译报错
byte number = (byte)128; // 不会报错
在Java
中,整数有4
种表现形式,如下:
-
二进制:以
0b
或0B
开头,如0b1001
。 -
八进制:以
0
开头,如0123
、-0123
。 -
十进制:如
120
,789
,1245
。 -
十六进制:以
0x
或0X
开头,如0x25
、0xb01e
。
并且在Java
中数据字面值可以加下划线,以增加代码的可读性,编译时编译器会去除这些下划线,如下:
int number = 0b1111_0011_0001;
System.out.println(number); // 3889
整数之间做运算时,有以下注意点:
-
两个
int
类型的数据做运算,结果仍是int
型。所以3 / 2
的结果为1
。 -
当多种数据类型在混合运算时,先将各自转换为容量最大的再做运算。
byte x = 10; int y = 100; long z = 1000L; // 程序报错, x、y、z都会转变为long类型,运算结果应该是long类型 int result = x + y +z;
-
short
、byte
、int
之间做运算时,各自先转换为int
,再做运算。short a = 12; byte b = 12; int c = a + b; int d = b + b; short e = b + b; // 报错, b + b的结果为int型
byte a = 10 / 3; // 不会报错,理解为一种优化 byte x = 10; byte y = 3; byte z = x / y; // 报错
3.6.2 浮点类型
浮点类型简称为【浮点型】,用来存储含有小数部分的数值。Java
中浮点型分为【单精度浮点型】和【双精度浮点型】,它们的精度不同。
类型 | 内存空间 | 精度 |
---|---|---|
float | 4 byte | 6-7 位小数 |
double | 8 byte | 15-16 位小数 |
**默认情况下小数都被看作double
型,若要使用float
型小数,则需要在小数后面加上f
或F
,否则编译器会报错。**除此之外,可以在小数后面加上D
或d
明确表示一个double
型数据,一般不加。
/*声明浮点型变量*/
float number1 = 1.23f;
double numner2 = 456.21d;
double number3 = 20.3;
// 编译器报错
float number3 = 10.2;
浮点型有以下两种表示方式:
- 十进制:
double x = 1.23
- 科学计数法:
double x = 1.23E2
当然,浮点型和整型类似,也存在自动类型转换和强制类型转换,如下:
byte -> short -> int -> long -> float -> byte
从上面知识点可知,long
为8 byte
,float
为4 byte
,那为什么float
的容量比long
的容量更大?这是因为浮点型的底层存储原理,如符号位、指数位和位数位。
浮点数属于近似值,在系统中运算后的结果可能与实际值有偏差。因此浮点数参与运算得出的结果一定不要使用==
来与其他数字做比较。那么浮点数就更不适合无法接受舍入误差的金融计算,例如System.out.println(2.0 - 1.1)
将打印出0.8999999999
,而不是人们期望的0.9
。这种舍入误差的主要原因是浮点数值采用二进制系统表示,而在二进制中无法精确的表示分数1/10
。如果在数值中不允许有任何舍入误差,就应该使用BigDecimal
类。
System.out.println(2.0 - 1.1); // 0.8999999999999999
3.6.3 字符类型
字符类型用于存储单个字符,内存空间为2 byte
,取值范围和short
类型一致。在定义字符类型变量时,要用单引号表示,如'a'
,如果使用双引号则表示一个字符串,如"a"
表示一个字符串。Java
中没有空字符,如char ch = '';
是不合法的,但是Java
中有空字符串。
/*声明字符类型变量*/
char ch1 = ' '; // 声明ch1为一个空白字符
char ch2 = '你';
在Java
中,char
类型统一采用Unicode
编码。在程序中尽量不要使用char
类型,能使用String
就使用String
。
【转义字符】是一种特殊的字符变量,它以反斜杠开头,后跟一个或多个字符。转义字符具有特定的含义,不同于字符原有的含义,不同于字符原有的含义,故称【转义】。
转义字符 | 含义 | Unicode 值 |
---|---|---|
\" | 双引号 | \u0022 |
\' | 单引号字符 | \u0027 |
\\ | 反斜杠字符 | \u005c |
\t | 垂直制表符 | \u0009 |
\r | 回车 | \u000d |
\n | 换行 | \u000a |
\b | 退格 | \u0008 |
\uxxxx | 4 位十六进制数据表示的字符,如\u000a |
public class Test {
public static void main(String[] args) {
char c1 = 32; // 空格
char c2 = '\u2605';
System.out.println(c2); // ★
// 字符和字符相加减会转换成码点值
System.out.println(c1 + c2); // 9765
String hello = "\u0048\u0065\u006c\u006c\u006f\u002c\u0020\u0057\u006f\u0072\u006c\u0064\u0021";
System.out.println(hello); // Hello, World!
}
}
与c
和c++
一样,Java
中也可以把字符当作整数对待。如果字面值没有超过short
的范围,则可以直接赋值给char
类型变量。若超过这个范围,则必须使用char
显示转换。
public class Test {
public static void main(String[] args) {
int unicode_number = 65230;
System.out.println((char)unicode_number); // ﻎ
}
}
char
类型也可以参与算术运算,将其作为short
类型的数值型看待即可。那么现在就可以得到完整的自动类型转换链:byte→short(char)→int→long→float→double
。
public class Test {
public static void main(String[] args) {
long number = 6L;
System.out.println(number * 'ﻎ'); // 391380
System.out.println(number + 'ﻎ'); // 65236
}
}
3.6.4 布尔类型
布尔类型又称逻辑类型,简称【布尔型】,通过关键字boolean
来定义布尔型变量。布尔类型只有true
和false
两个值,表示真和假,没有1
和0
这一说。布尔类型通常被用在流程语句中,作为判断语句。【整型值和布尔值之间不能进行相互转换】。
boolean b;
boolean b1, b2;
boolean b = true;
3.7 运算符
运算符的优先级为:自增自减 > 算数 > 比较 > 逻辑 > 赋值
,不知道优先级时直接加()
即可。
3.7.1 赋值运算符
赋值运算符是一个二元运算符,其作用是将右方的值赋值给左方的变量。
public class Test {
public static void main(String[] args) {
// 将 12 赋值给number
int number = 12;
System.out.println(number); // 12
// number += 6 即 number = number + 6
number += 6;
System.out.println(number); // 18
number -= 8;
System.out.println(number); // 10
number /= 2;
System.out.println(number); // 5
number *= 5;
System.out.println(number); // 25
number %= 3;
System.out.println(number); // 1
// 除此之外还有<<=、>>=、>>>=、&=、|=、~=、^=
}
}
/*
Java中也支持连续赋值,但是开发中最好分开赋值,这样代码层次分明
*/
int a, b, c;
a = b = c = 20;
System.out.println(a + " " + b + " " + c); // 20 20 20
当=
两侧类型数据不一样时,可以使用自动类型转换或强制类型转换来进行处理。但使用+=、-=、/=、%=
等赋值运算符时自带强制类型转换,即右端类型不能隐式转换为左端类型时,也不需要手动显式转换。
byte number = 10;
number = number + 5; // 程序报错
number = (byte)(number + 5); // 程序不会报错
number += 5; // 程序不会报错
3.7.2 算数运算符
算数运算符,即+
、-
、*
、/
、%
。其中+
、-
还可以作为数值的正负号。
import java.util.Scanner;
public class Test {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
System.out.println("请输入number1和number2的值: ");
double number1 = scan.nextDouble();
double number2 = scan.nextDouble();
System.out.println("number1 + number2 = " + (number1 + number2));
System.out.println("number1 - number2 = " + (number1 - number2));
System.out.println("number1 * number2 = " + (number1 * number2));
System.out.println("number1 / number2 = " + (number1 / number2));
System.out.println("number1 % number2 = " + (number1 % number2));
scan.close();
}
}
3.7.3 自增自减运算符
自增自减运算符是单元运算符,可以放在操作元之前,也可以放在操作元之后。
++a(--a) // 表示在使用变量a之前,先将a的值加(减)1
a++(a--) // 表示在使用变量a之后,将a的值加(减)1
b = ++a; // 先将a加1,然后赋值给b
b = a++; // 先将a的赋值给b,然后a的值加1
public class Test {
public static void main(String[] args) {
int a = 6;
int b = a++;
System.out.println("a的值为" + a + '\n' + "b的值为" + b); // a的值为7 b的值为6
int c = 6;
int d = ++c;
System.out.println("c的值为" + c + '\n' + "d的值为" + d); // c的值为7 d的值为7
}
}
3.7.4 比较运算符
比较运算符属于二元运算符,用于程序中的变量之间、变量与自变量之间以及其它类型的信息之间的比较。比较运算符的运算结果是boolean
型。比较运算符有6
种,>
、>=
、<
、<=
、==
、!=
。
public class Test {
public static void main(String[] args) {
int number1 = 10;
int number2 = 20;
System.out.println("number1 > number2 ----> " + (number1 > number2));
System.out.println("number1 < number2 ----> " + (number1 < number2));
System.out.println("number1 >= number2 ----> " + (number1 >= number2));
System.out.println("number1 <= number2 ----> " + (number1 <= number2));
System.out.println("number1 == number2 ----> " + (number1 == number2));
System.out.println("number1 != number2 ----> " + (number1 != number2));
}
}
/*
number1 > number2 ----> false
number1 < number2 ----> true
number1 >= number2 ----> false
number1 <= number2 ----> true
number1 == number2 ----> false
number1 != number2 ----> true
*/
3.7.5 逻辑运算符
逻辑运算符包括逻辑与&&(&)
、逻辑或||(|)
、逻辑非!
、逻辑异或^
。除了逻辑非是一元运算符,其它两个都是二元运算符。且&&
与||
的结合方向是从左往右,!
的结合方向是从右往左。
&&
和&
都表示逻辑与,那它们有什么区别呢?&&
属于【短路】运算符,即左端表达式能判断出真假就不会再去看右端表达式,从而节省计算机判断的次数。而&
需要执行逻辑运算符两端的表达式才能推断出整个表达式的值,所以&
也叫做【非短路】运算符。
public class Test {
public static void main(String[] args) {
int boys = 15;
int girls = 17;
int total = boys + girls;
boolean result1 = boys > girls && total > 30;
boolean result2 = boys > girls || total > 30;
System.out.println("男生人数大于女生人数并且总人数大于30 -----> " + result1); // false
System.out.println("男生人数大于女生人数并或总人数大于30 -----> " + result2); // true
}
}
3.7.6 位运算符
位运算符用于处理整数,包括byte、short、int、long
。位运算符是完全针对位方面的操作,整数在内存中以二进制的形式表示,负数在内存中以补码形式表示。若两个操作数的精度不同,则结果的精度与精度高的操作数相同。
位运算符 | 描述 |
---|---|
按位与& | 对应位都是1 ,结果位才为1 ,否则为0 |
按位或` | ` |
按位异或^ | 对应位相同时为0 ,否则为1 |
按位取反~ | 二进制中的1 修改为0 ,0 修改为1 |
左移<< | 按照指定位数向左移动,空缺位补0 |
右移>> | 按照指定位数向右移动,如果最高位为1 ,则空缺位补1 ;如果最高位为0 ,空缺位补0 |
无符号右移>>> | 按照指定位数向左移动,无论最高位是0 还是1 ,空缺位补0 |
左移右移的应用:向左移动n
位,就是将这个数乘以2^n
,向右移动n
位,就是将这个数除以2^n
。
3.7.7 三元运算符
三运运算符的使用格式为【条件式 ? 表达式1 : 表达式2
】,若条件式的值为true
,则执行表达式1
,否则执行表达式2
。能使用的三元运算符的一定可以改写为if...else...
的形式,但是不是所有的if...else...
都可以用三元运算符表示。【三元运算符的执行效率要比if...else...
要高】。
boolean b = 20 < 45 ? true : false;
// 等价与if...else...
boolean b;
if (20 < 45){
b = true;
} else {
b = false;
}
3.8 控制流程
3.8.1 条件语句
-
if
语句if (condition) { // 语句1 // 语句2 // ... } // 如果if语句中只有一条语句,则可以省略{} if(conition) 语句
-
if...else...
语句if (condition) { // } else { // }
-
if..else if...else
多分支语句if (condition) { // } else if (condition) { // } else if (condition) { // } else { }
3.8.2 分支语句
switch-case
多分支语句:case
标签必须是整型、字符、字符串或枚举类型,如果是枚举类型则不需要在每个标签中指明枚举名,可以由switch
的表达式推导出来。
switch (表达式) {
// break用于跳出switch语句,否则程序会一直往下执行
case 1: // 语句1; break;
case 2: // 语句2; break;
case 3: // 语句3; break;
case 4: // 语句4; break;
......
case n-1: // 语句n-1; break;
case n: // 语句n; break;
default:
// 默认语句,当上面case语句都没有执行时执行
enum Size {
SMALL,
MIDDLE,
BIG
}
Size sz = xx;
switch (sz) {
case SMALL: // 不用写Size.SMALL
//
break;
case MIDDLE:
//
break;
case BIG:
//
break;
}
在java12
中引入了新特性,使代码变得更加简洁。
public static void main(String[] args) {
int number = 1;
switch (number) {
case 1:case 2: case 3:
System.out.println("小点数");
break;
case 4: case 5: case 6:
System.out.println("大点数");
break;
default:
System.out.println("错误点数");
}
}
// 优化
public static void main(String[] args) {
int number = 1;
switch (number) {
case 1,2,3 -> System.out.println("小点数");
case 4,5,6 -> System.out.println("大点数");
default -> {
System.out.println("错误点数");
System.out.println("aaa");
}
}
}
3.8.3 循环语句
-
while
循环语句while (condition) { // // } // 如果while循环中只有一条语句,则可以简写 while (condition) // 语句
-
do...while
循环语句:至少执行一次循环。do { // } while(condition);
-
for
循环语句for(表达式1; 表达式2; 表达式3) { // 表达式1:初始化计数器 // 表达式2:每一次新循环执行前要检测的循环条件 // 表达式3:制定如何更新计数器 }
-
foreach
语句for (元素类型 x: 遍历对象obj) { // }
// foreach语句遍历一维数组 int[] arr = {1, 20, 65, 89}; for (int x : arr) { System.out.print(x + " "); // 1 20 65 89 } // foreach语句遍历二维数组 int[][] ar = {{1, 3, 2}, {5, 878}, {45, 23}}; for (int[] i : ar) { for (int j : i) { System.out.print(j + " "); // 1 3 2 5 878 45 23 } }
3.8.4 break
与continue
-
break
:跳出当前循环。循环嵌套下,break
语句只会使程序流程跳出包含它的最内层循环,即只跳出一层循环。 -
continue
:跳过本次循环。 -
带标的
break
语句和continue
语句:通过添加标签的方式,可以使break
语句跳出指定循环,使continue
语句跳过指定循环的本次循环。标签名: 循环体 { break 标签名; //continue 标签名 }
3.9 方法
在Java
中,方法**Method
** 是类或对象行为的封装,用于执行特定任务。方法由名称、参数列表、返回类型和方法体组成,是面向对象编程中实现代码复用和模块化的核心机制。方法的本质就是一段可以重复利用的代码片段,可以完成一个功能。方法定义在类中,所以又称【成员方法】。
3.9.1 方法的定义
方法的基本语法如下:
[访问修饰符] [static] [final] 返回类型 方法名(参数列表) [throws 异常] {
// 方法体
return 返回值; // 若返回类型非 void
}
-
修饰符:如
public
、private
、static
等。 -
返回类型:对于方法的【返回值类型】,可以是
Java
语言中的任何一种数据类型,如int
、String
等。如果方法执行结束不返回任何数据给调用者,则返回值类型写void
。当返回值类型不为void
时,必须使用return
返回数据。程序中只要遇到return
,则会结束该方法。return
有以下两种写法:-
return returnData;
,用于返回指定类型的数据。 -
return;
,用于在void
方法中结束方法执行。
-
-
方法名:符合标识符规范即可。
-
参数列表:传入方法的参数,可以为空或包含多个参数,称为【形参】。调用方法时传入的参数称为【实参】。
-
方法体:包含具体逻辑的代码块。
3.9.2 方法的调用
方法分为类方法【static
修饰的方法】和实例方法,调用也有细微区别,如下:
- 类方法:即静态方法,通过类名直接调用,如
Math.max(1,2)
。 - 实例方法:即非静态方法,通过实例对象调用,如
obj.fun()
。
如果是在同一个类中的方法,无论是静态方法还是非静态方法,都可以直接调用,无需使用类名或对象。
3.9.3 参数传递
基本类型:值传递,传递副本,方法内修改不影响原值。
public class ParameterDemo {
public static void main(String[] args) {
int x = 10;
System.out.println("调用前: x = " + x); // 输出 10
modifyValue(x);
System.out.println("调用后: x = " + x); // 输出 10(未改变)
}
static void modifyValue(int num) {
num = 20; // 修改的是副本
System.out.println("方法内: num = " + num); // 输出 20
}
}
引用数据类型:引用传递,传递引用的副本,方法内修改对象属性会影响原对象。
public class ParameterDemo {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder("Hello");
System.out.println("调用前: " + sb); // 输出 Hello
modifyObject(sb);
System.out.println("调用后: " + sb); // 输出 Hello World
}
static void modifyObject(StringBuilder builder) {
builder.append(" World"); // 修改的是原对象的内容
System.out.println("方法内: " + builder); // 输出 Hello World
}
}
3.9.4 方法执行时的内存变化
Java
内存模型概述如下:
- 栈内存
Stack
- 存储方法的调用信息【栈帧】和局部变量。
- 每个线程都有独立的栈,遵循先进后出原则。
- 堆内存
Heap
- 存储对象实例和数组。
- 所有线程共享,由
GC
管理。
- 方法去
Method Area
:存储类信息、常量、静态变量等。
方法调用时的栈内存变化的基本流程如下:
- 每个方法调用会创建一个栈帧,包括局部变量表、操作数栈、动态链接、方法返回地址。
- 方法执行结束,栈帧弹出,释放内存。
public class MemoryDemo {
public static void main(String[] args) {
int a = 10;
int b = 20;
int result = add(a, b);
System.out.println(result);
}
static int add(int x, int y) {
int sum = x + y;
return sum;
}
}
通过分析代码来解释内存变化步骤,如下:
-
main
方法调用-
栈内存中压入
main
方法的栈帧。 -
局部变量
a=10
、b=20
存储在局部变量表中。|------------------| | main 方法栈帧 | | a=10, b=20 | |------------------|
-
-
调用
add(a, b)
-
参数
a
、b
的值被复制到add
方法的x
、y
。 -
压入
add
方法的栈帧。|------------------| | add 方法栈帧 | | x=10, y=20 | |------------------| | main 方法栈帧 | | a=10, b=20 | |------------------|
-
-
执行
add
方法-
计算
sum=x+y
,局部变量sum=30
存储在add
的栈帧中。|------------------| | add 方法栈帧 | | x=10, y=20, sum=30| |------------------| | main 方法栈帧 | | a=10, b=20 | |------------------|
-
-
add
方法返回-
return sum
将30
返回给main
方法。 -
add
的栈帧弹出,内存释放。|------------------| | main 方法栈帧 | | a=10, b=20, result=30| |------------------|
-
-
main
方法结束:main
栈帧弹出,程序终止。
3.9.5 方法的重载
在 Java
中,方法重载Overload
允许在同一个类中定义多个同名方法,但要求它们的参数列表不同。这是实现【多态性】的一种重要方式,能够简化代码调用,提高可读性和灵活性。当满足以下三个条件任意一个条件时即可构成方法重载:
条件 | 合法示例 | 非法示例【编译错误】 |
---|---|---|
参数类型不同 | void print(int num) 和 void print(String text) | void log(int x) 和 int log(int y) |
参数数量不同 | int add(int a, int b) 和 int add(int a, int b, int c) | void process() 和 String process() |
参数顺序不同 | void setInfo(String name, int age) 和 void setInfo(int age, String name) | void save(int x, double y) 和 void save(double y, int x) 【合法,但注意调用歧义】 |
对于方法的重载,要特别注意:经常使用的System.out.println()
其实就是方法的重载。
-
返回类型不同不构成重载。
-
参数名称不同不构成重载。
-
可变参数:
int sum(int... nums)
和int sum(int a, int b)
是合法的重载。 -
当调用重载方法时,编译器优先选择最精确匹配的参数类型。若没有精确匹配,会尝试隐式类型转换。
public class OverloadDemo { void process(int x) { System.out.println("int"); } void process(double x) { System.out.println("double"); } void process(Integer x) { System.out.println("Integer"); } void process(String x) { System.out.println("String"); } } public static void main(String[] args) { OverloadDemo demo = new OverloadDemo(); demo.process(10); // 输出 "int"(精确匹配) demo.process(10.0); // 输出 "double" demo.process(10L); // 输出 "double"(long → double) demo.process(10.5f); // 输出 "double"(float → double) demo.process((Integer)10); // 输出 "Integer"(精确匹配包装类) demo.process("10"); // 输出 "String" }
3.9.6 方法递归调用的原理
在 Java
中,方法递归调用Recursion
是指一个方法直接或间接调用自身的过程。如下代码就会发生栈溢出:
public class DruidConfig {
public static void main(String[] args) {
fun();
}
static void fun() {
System.out.println("递归调用");
fun();
}
}
因此在实际开发中,递归方法必须满足两个条件,否则会出现无限递归,最终出现StackOverflowError
。
- 终止条件:递归终止的条件,当满足该条件时,不再调用自身,直接返回结果。
- 递归条件:将原问题拆解为更小的同类子问题,继续调用自身。
也因此,开发中能使用迭代就是用迭代【循环】,尽量不要使用递归。如果在实际开发中因为递归发生内存溢出错误,应该怎么办?
- 首先可以调整栈内存的大小。
- 如果没用就因为检查自己的递归结束条件是否不对。
递归的常见应用如下:
-
斐波拉契数列
public static int fibonacci(int n) { if (n <= 1) { // 终止条件 return n; } return fibonacci(n - 1) + fibonacci(n - 2); }
-
文件夹遍历
public void listFiles(File dir) { File[] files = dir.listFiles(); if (files != null) { for (File file : files) { if (file.isDirectory()) { listFiles(file); // 递归遍历子目录 } else { System.out.println(file.getPath()); } } } }
3.10 package
和import
在 Java
中,包Package
机制是代码组织和模块化的核心工具,用于管理类的命名空间、访问权限以及代码结构。以下是包机制的详细解析,涵盖其核心作用、使用规则和实际应用场景。包的核心作用如下:
功能 | 说明 |
---|---|
命名空间管理 | 避免类名冲突,如不同包中的同名类 com.util.Date 和 java.util.Date |
访问控制 | 配合访问修饰符,public 、protected 、默认包级私有)限制类的可见性 |
代码模块化 | 按功能/模块划分代码,如 com.example.dao 、com.example.service |
物理目录映射 | 包名对应文件系统的目录结构,如包 com.example.util → 目录 com/example/util |
包的定义与使用规则如下:
-
包声明:使用
package
语句,必须放在.java
文件中的首行,注释除外。未声明package
的类属于默认包,无法被其他包直接访问,不推荐使用。package com.example.util; // 定义类所属的包
-
包命名规范:全小写字母,采用域名反转,如
com.person.commom
,并且不能使用java
、int
等。 -
包名必须与
.java
文件的物理路径完全一致。 -
包的访问机制:包机制与访问修饰符共同控制类的可见性。
访问修饰符 同类 同包 子类 其他包 public
✔️ ✔️ ✔️ ✔️ protected
✔️ ✔️ ✔️ ❌ 默认【包级私有】 ✔️ ✔️ ❌ ❌ private
✔️ ❌ ❌ ❌
包的导入使用import
语句,如下:
-
导入单个类
import java.util.ArrayList; // 导入 ArrayList
-
导入整个包【慎用】
import java.util.*; // 导入 java.util 包下所有类 不包含子包
-
静态导入
import static java.lang.Math.PI; // 导入静态常量 import static java.lang.System.out; // 导入静态方法
-
完全限定名:避免冲突
// 同时需要 java.util.Date 和 java.sql.Date 时 java.util.Date date1 = new java.util.Date(); java.sql.Date date2 = new java.sql.Date(...);
3.11 输入和输出
3.11.1 输入
读取输入使用java.util.Scanner
类。要想通过控制台进行输入,首先需要构造一个标准输入流System.in
关联的Scanner
对象。
-
Scanner(InputStream in)
:用给定的输入流创建一个Scanner
对象。 -
xxx nextXXX()
:读取输入流中的指定基本数据类型的数据,不包括字符和字符串。 -
String next()
与String nextLine()
:读取输入流中的数据,返回值为String
类型。两者不同的是,前者以空白作为结束符,后者以回车作为结束符。public class Scanner_demo { public static void main(String[] args) { Scanner sc = new Scanner(System.in); System.out.println("请输入一句话:"); String str = sc.next(); // 你好世界 hahaha System.out.println(str); // 你好世界 sc.close(); } }
public class Scanner_demo { public static void main(String[] args) { Scanner sc = new Scanner(System.in); System.out.println("请输入一句话:"); String str = sc.nextLine(); // 你好世界 hahaha System.out.println(str); // 你好世界 hahaha sc.close(); } }
-
char next().charAt(0)
:读取输入流中数据的第一个字符。 -
BigInteger nextBigInteger()
:读取输入流中的BigInteger类型的数字。 -
BigDecimal nextBigDecimal()
:读取输入流中的BigDecimal类型的数字。 -
boolean hasNextXXX()
:判断输入流中的数据是否为指定数据类型,包括BigInteger
、BigDecimal
。public class Main { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); System.out.println("请输入一些整数,用空格分隔:"); while (scanner.hasNextInt()) { // 从输入流中依次拿取数据,判断数据是否为int类型 int number = scanner.nextInt(); // 依次读取输入流中的数据 System.out.println("你输入的整数是: " + number); } scanner.close(); } }
-
boolean hasNext()
与boolean hasNextLine()
:检查输入流中是否还有数据可以读取。public class Scanner_demo { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); while (scanner.hasNext()) { String line = scanner.nextLine(); System.out.println("输入的内容是:" + line); } } sc.close(); }
在调用 nextXXX()
之前调用 hasNextXXX()
是一个好习惯,因为这可以防止程序在没有有效输入的情况下尝试读取数据,从而导致 InputMismatchException
异常。
当你运行这个程序并调用 hasNextXXX()
时,程序会等待你输入数据。这是因为 hasNextXXX()
只是检查是否有输入可用,而不会读取任何数据。一旦你输入了一些数据并按下回车,hasNextXXX()
将返回 true
,然后你可以调用 next()
来读取你输入的数据。
3.11.2 格式化输出
使用System.out.printf()
可以实现格式化输出。
转换符 | 说明 |
---|---|
%b 、%B | 布尔类型 |
%h 、%H | 散列码 |
%s 、%S | 字符串类型 |
%c 、%C | 字符类型 |
%d | 十进制整数 |
%o | 八进制整数 |
%x 、%X | 十六进制整数 |
%f | 定点浮点型 |
%e | 指数浮点数 |
%% | 字面值% |
public class Arrays_demo {
public static void main(String[] args) {
System.out.printf("%d + %d = %d", 12, 12, 24);
System.out.println();
double dou = 1.2566455455;
// 保留三位小数,四舍五入
System.out.printf("%.3f", dou);
}
}
4. 面向对象
4.1 面向对象与面向过程的区别
面向对象 OOP
和面向过程POP
是两种不同的编程范式,它们在设计思路、代码组织方式、数据与行为的关系等方面有显著区别。以下是它们的核心差异及示例说明:
维度 | 面向过程 | 面向对象 |
---|---|---|
关注点 | 以步骤为中心,关注怎么做 | 以对象为中心,关注谁来做和做什么。 |
程序结构 | 由一系列函数【过程】组成,按顺序执行 | 由对象【类】组成,对象之间通过消息传递协作 |
数据与行为关系 | 数据【变量】和函数分离,函数操作外部数据 | 数据【属性】和行为【方法】封装在对象内部 |
可以对比一下面向对象和面向过程的代码结构:
-
面向过程
// 计算矩形面积和周长 #include <stdio.h> // 定义数据结构和函数 struct Rectangle { float length; float width; }; float calculateArea(struct Rectangle rect) { return rect.length * rect.width; } float calculatePerimeter(struct Rectangle rect) { return 2 * (rect.length + rect.width); } int main() { struct Rectangle rect = {5.0, 3.0}; printf("面积: %.2f\n", calculateArea(rect)); printf("周长: %.2f\n", calculatePerimeter(rect)); return 0; }
-
面向过程
// 矩形类封装数据和行为 public class Rectangle { private double length; private double width; public Rectangle(double length, double width) { this.length = length; this.width = width; } public double calculateArea() { return length * width; } public double calculatePerimeter() { return 2 * (length + width); } public static void main(String[] args) { Rectangle rect = new Rectangle(5.0, 3.0); System.out.println("面积: " + rect.calculateArea()); System.out.println("周长: " + rect.calculatePerimeter()); } }
核心区别如下:
- 面向过程
- 数据和函数分离
- 函数需要显示操作外部数据,数据可能被多个函数随意更改
- 扩展性:新增功能可能需要修改现有函数或数据结构,容易引发连锁问题
- 面向对象
- 数据和方法封装在类内部
- 通过访问控制限制外部直接修改数据,提高安全性
- 扩展性:通过多态和接口扩展功能,符合开闭原则
面向过程和面向对象的优缺点对比和适用场景如下:
范式 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
面向过程 | - 简单直观,适合小型程序 - 执行效率高 | - 难以应对复杂需求 - 数据与逻辑分离导致维护困难 | - 简单脚本工具,如数据处理 - 性能敏感的底层开发,如操作系统驱动 |
面向对象 | - 易维护、易扩展 - 适合大型系统开发 | - 学习成本高 - 过度设计可能导致性能下降 | - 企业级应用,如电商系统 - 需要长期维护和扩展的软件 |
Java
语言的权限修饰符有4
种,分别是public、private、protected、缺省
。类中属性一般声明为private
,方法声明为public
。
权限修饰符 | 范围 |
---|---|
private | 类内部 |
缺省 | 包内部 |
protected | 包内部及其其它包的子类 |
public | 任意位置 |
4.2 类和对象
在现实世界中,事物与事物之间具有共同特征,将这些共同的特征和行为提取出来,形成一个模板,称为【类】。类是一个抽象的概念,特征对应类中的属性,行为对应类中的方法,【类 = 属性 + 方法】。类的基本语法如下:
[访问修饰符] class 类名 [extends 父类] [implements 接口1, 接口2, ...] {
// 成员变量(属性)
[访问修饰符] 数据类型 变量名;
// 构造方法
[访问修饰符] 类名(参数列表) {
// 初始化代码
}
// 成员方法(行为)
[访问修饰符] 返回类型 方法名(参数列表) {
// 方法体
}
}
// 定义一个学生类
public class Student {
private String name;
private int age;
public Student () {}
public Student(String name, int age) {
this.age = age;
this.name = name;
}
public String getName() {
return this.name;
}
}
对象是实际存在的个体,通过类可以实例化对象,例如人的类中有布鲁斯、蔡徐坤。对象的基本使用如下:当访问的对象不存在是会出现空指针异常。
public static void main(String[] args) {
Student stu = new Student("布鲁斯", 18);
System.out.println(stu.getName()); // 布鲁斯
stu.age = 30;
System.out.println(stu.age); // 30
}
创建的对象会存放在【堆内存】中,元空间里面存储的是类的模板,如成员方法、类变量等。
4.3 this
的使用
在 Java
中,this
关键字用于表示当前对象的引用,其主要作用是明确访问当前对象的成员变量或方法,避免命名冲突或混淆。this
存储在栈帧的局部变量表的第0
个槽位上。以下是this
的使用场景:
-
区分成员变量和局部变量
public class Person { private String name; // 成员变量 public void setName(String name) { // 参数与成员变量同名 this.name = name; // this.name 表示成员变量, 右侧 name 是参数 } }
-
在构造方法中调用其它构造方法:通过
this()
调用当前类的其他构造方法,必须位于构造方法的首行。public class Car { private String brand; private int price; public Car() { this("Unknown"); // 调用带参构造方法 } public Car(String brand) { this(brand, 0); // 调用两个参数的构造方法 } public Car(String brand, int price) { this.brand = brand; this.price = price; } }
-
返回当前对象
public class Student { private String name; private int age; public Student setName(String name) { this.name = name; return this; // 返回当前对象 } public Student setAge(int age) { this.age = age; return this; } } // 链式调用示例 Student student = new Student() .setName("Alice") .setAge(20);
-
像其它方法传递当前对象
public class Printer { public void printInfo(Student student) { System.out.println("Name: " + student.getName()); } } public class Student { private String name; public void sendToPrinter(Printer printer) { printer.printInfo(this); // 将当前 Student 对象传递给 Printer } }
-
在内部类中访问外部类对象:在非静态内部类中,通过
外部类名.this
访问外部类的成员。public class Outer { private int value = 10; public class Inner { public void printOuterValue() { System.out.println(Outer.this.value); // 访问外部类成员 } } }
对于this
,是可以省略的,默认访问的就是当前对象。静态方法中不能使用this
。
4.4 封装
OOP
的三大核心特性就是封装、继承和多态。【封装】就是属性和方法绑定在类中,对外隐藏实现细节,仅通过公开的接口访问。其核心思想如下:
-
隐藏内部细节:通过访问修饰符控制其可见性,一般将其私有化。
-
暴露必要接口:通过公共方法
Getter
、Setter
操作私有属性。public class BankAccount { // 私有属性,外部无法直接访问 private double balance; // 公开方法操作私有属性 public void deposit(double amount) { if (amount > 0) { balance += amount; } } public double getBalance() { return balance; } }
4.5 构造方法
构造器Constructor
是一种特殊类型的方法,即构造方法。构造方法的使用注意点如下:
-
构造方法支持重载。
-
构造方法与类名一致,且没有返回值。
-
构造方法在创建对象时执行。
-
如果类中没有显式定义,系统会默认提供一个无参构造方法。反之则不会提供默认的无参构造方法。
public class Employee { // String型的成员变量name、id public String name; public String id; protected String address; // 无参构造器 public Employee() { } // 含参构造器 public Employee(String name, String id, String address) { this.address = address; this.id = id; this.name = name; } public static void main(String[] args) { // 使用构造器及性能初始化 Employee employee = new Employee("布鲁斯", "1002", "重庆市"); System.out.println(employee.name + " " + employee.id + " " + employee.address); // 布鲁斯 1002 重庆市 } }
4.6 初始化块
在java
中,初始化块分为静态代码块和实例代码块。使用区别如下:
-
静态代码块:使用
static {}
定义,属于类级别。类加载到内存时执行,仅执行一次。常用于初始化静态变量或执行只需一次的静态资源,如加载配置文件、注册驱动等。静态代码块中只能访问静态资源,且静态代码块中的异常必须捕获并处理。public class DatabaseConfig { // 静态变量 private static String url; private static String username; // 静态代码块 static { System.out.println("静态代码块执行:加载数据库配置"); url = "jdbc:mysql://localhost:3306/test"; username = "root"; // 加载 JDBC 驱动 try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static void printConfig() { System.out.println("URL: " + url); System.out.println("Username: " + username); } public static void main(String[] args) { DatabaseConfig.printConfig(); // 静态代码块执行:加载数据库配置 // URL: jdbc:mysql://localhost:3306/test // Username: root } }
-
实例代码块:直接使用
{}
定义,属于对象级别。每次创建对象时执行,在构造方法前执行。常用于初始化实例变量或为多个构造方法提供共享的代码。实例代码块中可以访问所有成员。public class Car { // 实例变量 private String color; private int speed; // 实例代码块 { System.out.println("实例代码块执行:默认初始化"); color = "黑色"; speed = 0; } // 构造方法 1 public Car() { System.out.println("无参构造方法执行"); } // 构造方法 2(带参数) public Car(String color) { this.color = color; System.out.println("带参构造方法执行"); } public static void main(String[] args) { Car car1 = new Car(); // 实例代码块执行:默认初始化 // 无参构造方法执行 Car car2 = new Car("红色"); // 实例代码块执行:默认初始化 // 带参构造方法执行 } }
4.7 static
关键字
使用static
声明的静态变量和静态方法都存储在方法区中。在Java
中,static
关键字用于定义类级别的成员,其行为和内存管理与实例成员有本质区别。下面是类成员:
- 静态变量:即类变量,类加载时分配在方法区【元空间】,所有对象共享一份数据。常用于全局配置,或需要共享的计数器或缓存数据。
- 静态方法:常用于工具方法、工厂方法。
- 静态代码块
- 静态内部类
类成员与实例成员的区别如下:
特性 | 类成员 | 实例成员 |
---|---|---|
归属 | 类级别【static 修饰】 | 对象级别【无 static 】 |
内存分配 | 类加载时分配在方法区Metaspace | 对象创建时分配在堆内存 |
访问方式 | 类名.成员名 (如 Math.PI ) | 对象名.成员名 【如 person.getName() 】 |
生命周期 | 从类加载到程序结束 | 从对象创建到 GC 回收 |
线程安全性 | 需同步控制【共享数据】 | 默认线程私有【除非共享对象引用】 |
4.8 单例模式
单例模式是一种设计模式,用于确保一个类只有一个实例,并提供全局访问点。
-
饿汉式单例模式:类加载时立即创建实例,线程安全,优点是实现简单,缺点是浪费资源,实例未使用时也占用内存。
public class EagerSingleton { private static final EagerSingleton INSTANCE = new EagerSingleton(); private EagerSingleton() {} // 私有构造方法 public static EagerSingleton getInstance() { return INSTANCE; } }
-
懒汉式单例模式:延迟实例化,但是多线程环境下不安全,可能创建多个实例。
public class LazySingleton { private static LazySingleton instance; private LazySingleton() {} public static LazySingleton getInstance() { if (instance == null) { instance = new LazySingleton(); } return instance; } }
-
懒汉式【加锁】:通过
synchronized
保证线程安全,但性能较差,每次调用都加锁。public class SynchronizedSingleton { private static SynchronizedSingleton instance; private SynchronizedSingleton() {} public static synchronized SynchronizedSingleton getInstance() { if (instance == null) { instance = new SynchronizedSingleton(); } return instance; } }
4.9 继承
在 Java
中,继承Inheritance
是面向对象编程的核心机制之一,允许子类【派生类】继承父类【基类】的属性和方法,实现代码复用和层次化设计。使用extends
关键字实现继承,如下:
// 父类
class Animal {
public void eat() {
System.out.println("Animal is eating");
}
}
// 子类继承父类
class Dog extends Animal {
public void bark() {
System.out.println("Dog is barking");
}
}
// 使用
Dog dog = new Dog();
dog.eat(); // 继承自 Animal
dog.bark(); // 子类新增方法
继承的核心规则如下:
-
访问权限
-
子类可以继承父类的
public
和protected
成员,不能继承private
成员。 -
默认【缺省】成员仅在同包内可继承。
-
-
Java
不支持多继承,但是支持多层继承。 -
在创建子类对象时,会优先调用父类的无参构造器。若父类中没有无参构造方法,子类必须显式的调用父类有参构造方法,不然程序会报错。
【方法重写overwrite
】当从父类继承的方法无法满足业务需求时,就可以进行方法的重写。重写规则如下:
规则 | 说明 |
---|---|
方法名与参数列表一致 | 子类方法必须与父类方法名称、参数类型及顺序完全一致 |
返回类型兼容 | 子类方法返回类型可以是父类方法返回类型的子类,协变返回类型 |
访问权限不能更严格 | 子类方法的访问修饰符权限需大于等于父类,如父类为 protected ,子类可为 public |
异常类型不能更宽泛 | 子类方法抛出的异常类型需是父类方法异常的子类或相同 |
重写的方法应该加上@Override
注解,标记之后编译器会检查是否符合重写规则。方法重写针对的是实例方法。
继承中的初始化顺序如下:
-
父类静态代码块 → 子类静态代码块
-
父类实例代码块 → 父类构造方法
-
子类实例代码块 → 子类构造方法
class Parent { static { System.out.println("Parent static block"); } { System.out.println("Parent instance block"); } Parent() { System.out.println("Parent constructor"); } } class Child extends Parent { static { System.out.println("Child static block"); } { System.out.println("Child instance block"); } Child() { System.out.println("Child constructor"); } } public class Test { public static void main(String[] args) { new Child(); } } /* Parent static block Child static block Parent instance block Parent constructor Child instance block Child constructor */
4.10 super
关键字
super
代表当前对象中的父类型特征,大部分情况是可以省略的,但是当子类与父类的属性或方法名相同时,则需要使用suprt
访问父类的属性或方法。this
是一个引用,但super
不是,所以super
不能单独输出。
-
访问父类成员:子类通过
super
调用父类被重写的方法或访问父类属性。class Parent { String value = "Parent"; } class Child extends Parent { String value = "Child"; void print() { System.out.println(super.value); // 输出 "Parent" System.out.println(this.value); // 输出 "Child" } }
-
调用父类构造方法:子类构造方法中必须首行调用父类构造方法。如果没有显式调用,则默认调用父类的无参构造方法,即
super()
。class Parent { Parent(int value) { System.out.println("Parent constructor"); } } class Child extends Parent { Child() { super(10); // 必须显式调用父类有参构造方法 System.out.println("Child constructor"); } }
4.11 向上转型与向下转型
在 Java
中,向上转型和 向下转型是面向对象编程中处理继承关系的核心机制。
-
向上转型:将 子类对象引用赋值给父类引用变量,属于自动类型转换,无需显式强制转换。本质就是父类引用指向子类对象。
class Animal { void eat() { System.out.println("Animal is eating"); } } class Dog extends Animal { @Override void eat() { System.out.println("Dog is eating"); } void bark() { System.out.println("Dog is barking"); } } public class Main { public static void main(String[] args) { // 向上转型 Animal animal = new Dog(); animal.eat(); // 输出 "Dog is eating"【多态】 // animal.bark(); // 错误!父类引用无法调用子类特有方法 } }
-
向下转型:将父类引用转换为子类类型,需使用强制类型转换。本质就是恢复子类的完整功能。向下转型就是为了使用子类独有的方法。
public class Main { public static void main(String[] args) { Animal animal = new Dog(); // 向上转型 // 向下转型:需显式强制转换 Dog dog = (Dog) animal; dog.bark(); // 输出 "Dog is barking" // 父类引用未指向 Cat 却强制转为 Cat // Cat cat = (Cat) animal; // 抛出 ClassCastException } }
4.12 多态
在 Java
中,多态Polymorphism
是面向对象编程的核心特性之一,指同一操作作用于不同对象时,可产生不同的行为。多态通过【继承】和【方法重写】实现,并依赖动态绑定机制在运行时确定具体调用的方法。Java
的多态分为以下两种:
-
编译时多态:静态多态
-
通过重载实现
-
在编译时确定使用的方法
class Calculator { int add(int a, int b) { return a + b; } double add(double a, double b) { return a + b; } // 重载 }
-
-
运行时多态:动态多态
-
通过重写和向上转型实现。
-
在运行时根据对象类型确定调用的方法。
class Animal { void sound() { System.out.println("Animal makes sound"); } } class Dog extends Animal { @Override void sound() { System.out.println("Dog barks"); } // 重写 }
运行时多态的实现条件如下:
-
继承关系:子类继承父类
-
方法重写:子类重写父类的方法
-
向上转型:父类引用指向子类对象
class Shape { void draw() { System.out.println("Drawing a shape"); } } class Circle extends Shape { @Override void draw() { System.out.println("Drawing a circle"); } } class Square extends Shape { @Override void draw() { System.out.println("Drawing a square"); } } public class Main { public static void main(String[] args) { Shape[] shapes = {new Circle(), new Square()}; for (Shape shape : shapes) { shape.draw(); // 多态调用 } } }
多态的限制与注意事项如下:
-
成员变量无多态性:成员变量由引用类型决定,而非实际对象。
class Parent { int value = 10; } class Child extends Parent { int value = 20; } Parent obj = new Child(); System.out.println(obj.value); // 输出 10【父类变量】
-
静态方法无多态性:静态方法调用由编译时类型决定。
class Parent { static void method() { System.out.println("Parent"); } } class Child extends Parent { static void method() { System.out.println("Child"); } } Parent obj = new Child(); obj.method(); // 输出 "Parent"
-
构造方法中的多态风险:在构造方法中调用可重写方法可能导致未初始化错误
class Parent { Parent() { print(); } // 危险操作! void print() { System.out.println("Parent"); } } class Child extends Parent { int value = 10; @Override void print() { System.out.println(value); } // 输出 0【未初始化】 }
-
在向下转型时,很容易出现类型转换错误ClassCastException
,为了避免这种情况发生,我们可以使用类型检查:
if (animal instanceof Dog) {
Dog dog = (Dog) animal;
dog.bark();
}
4.13 抽象类与抽象方法
在 Java
中,抽象类Abstract Class
是面向对象编程中用于定义部分实现并提供抽象方法的核心机制。在解决实际问题时,往往将父类定义为【抽象类】,使用abstract
关键字定义的类称为抽象类。抽象类中可以定义抽象方法,抽象方法也是用abstract
关键词定义。
public abstract class Animal {
// 抽象方法【无实现】
public abstract void makeSound();
// 具体方法【有默认实现】
public void sleep() {
System.out.println("Animal is sleeping");
}
}
抽象类的相关注意点如下:
- 抽象类使用
abstract
关键字声明,也有构造方法,不能被实例化,就是单纯给子类用的。 - 可以包含抽象方法与具体方法【有默认实现】。
- 必须被继承才能使用,且子类必须实现所有抽象方法。
- 抽象类中不一定有抽象方法,但是有抽象方法的类一定是抽象类。
abstract
关键字不能和final
、private
、static
关键字共存。
抽象类的核心特点如下:
特性 | 说明 |
---|---|
无法实例化 | 不能直接通过 new 创建对象 |
可包含抽象方法 | 抽象方法必须被非抽象子类实现 |
可包含具体方法 | 提供默认实现,子类可选择是否重写 |
可包含成员变量 | 支持定义普通变量、静态变量和常量【final 】 |
可包含构造方法 | 子类构造方法需通过 super() 调用抽象类的构造方法【用于初始化成员变量】 |
抽象类的常见使用场景如下:
-
定义模板方法模式:在父类中定义算法骨架,子类实现具体步骤。
public abstract class Game { abstract void initialize(); // 抽象步骤 abstract void startPlay(); abstract void endPlay(); // 模板方法[定义流程] public final void play() { initialize(); startPlay(); endPlay(); } } class Cricket extends Game { @Override void initialize() { System.out.println("Cricket Initialized"); } @Override void startPlay() { System.out.println("Cricket Started"); } @Override void endPlay() { System.out.println("Cricket Finished"); } }
-
提供部分公共实现代码:多个子类共享部分代码,避免重复。
public abstract class Shape { protected String color; public Shape(String color) { this.color = color; } public abstract double area(); // 强制子类实现 public void printColor() { System.out.println("Shape color: " + color); } } class Circle extends Shape { private double radius; public Circle(String color, double radius) { super(color); this.radius = radius; } @Override public double area() { return Math.PI * radius * radius; } }
-
强制子类规范行为:通过抽象方法强制子类实现特定功能。
public abstract class DatabaseConnector { public abstract void connect(); public abstract void disconnect(); } class MySQLConnector extends DatabaseConnector { @Override public void connect() { /* MySQL 连接逻辑 */ } @Override public void disconnect() { /* MySQL 断开逻辑 */ } }
**抽象类之间是可以继承的,子抽象类可以选择不实现父类的抽象方法,并且可以添加新的抽象方法。**抽象类也可以继承具体类,和继承抽象类类似。
4.14 final
关键字
在 Java
中,final
关键字用于限制变量、方法或类的可变性和继承性,是提高代码安全性和可维护性的重要工具。
-
局部变量:不可重新赋值,声明时必须进行初始化,且后续不可修改。
public void example() { final int x = 10; // x = 20; // 错误!无法修改 final 变量 }
-
成员变量:必须在构造方法或声明时进行初始化。
class MyClass { final int value; // 必须在构造方法中初始化 public MyClass(int value) { this.value = value; } }
-
静态变量:必须在声明时后静态代码块中进行初始化,这样的静态变量也叫做【常量】。
class Constants { static final double PI = 3.14159; // 声明时初始化 static final int MAX_SIZE; static { MAX_SIZE = 100; // 静态代码块中初始化 } }
-
引用变量:引用不可变,但对象内部状态可变。
final List<String> list = new ArrayList<>(); list.add("Hello"); // 允许修改对象内容 // list = new ArrayList<>(); // 错误!不可修改引用
当然,final
也可以修饰方法和类:final
关键字不能用来定义抽象类和接口
-
修饰方法:禁止子类重写
class Parent { public final void doSomething() { System.out.println("Parent's final method"); } } class Child extends Parent { // @Override // 错误!无法重写 final 方法 // public void doSomething() { ... } }
-
修饰类:禁止继承,无法创建子类,如
String
final class StringUtils { // 工具类通常设为 final public static String reverse(String s) { return new StringBuilder(s).reverse().toString(); } } // class SubUtils extends StringUtils { ... } // 错误!无法继承 final 类
-
修饰参数:方法参数不可修改
public void process(final int param) { // param = 10; // 错误!不可修改 final 参数 }
4.15 接口
【接口】是抽象类的延伸,可以看作成是纯粹的、特殊的抽象类,使用interface
定义接口。接口由【全局静态常量】和【公共的抽象方法】组成,接口是全抽象的,连构造方法都没有。一个类虽然只支持单继承,但是可以同时实现多个接口,如class Dog extends Animal implements A, B, C {}
。
-
接口中定义的方法都默认为
public stract
,可以省略。 -
接口中定义的字段都默认为
public static final
,可以省略,实现该接口中的子类必须重写接口中所有抽象方法。 -
接口与接口间可以继承。
interface Person { String DATE = "2024-4-8"; void setName(String name); String getName(); } class Child { void fun() { System.out.println("天真孩童"); } } class Bruce extends Child implements Person{ String name; Bruce(String name) { this.name = Objects.requireNonNullElse(name, "unknown"); } @Override public void setName(String name) { this.name = name; } @Override public String getName() { return this.name; } }
在Java
中,接口从Java 8
开始支持声明静态方法static methods
和默认方法default methods
。接口中可以定义多个静态方法和默认方法。
-
静态方法
public interface MyInterface { // 静态方法 static void staticMethod() { System.out.println("This is a static method in the interface."); } // 其他抽象方法... } // 使用时 MyInterface.staticMethod(); // 直接通过接口名调用静态方法
-
默认方法
public interface MyInterface { // 默认方法 default void defaultMethod() { System.out.println("This is a default method in the interface."); } // 其他抽象方法... } class MyClass implements MyInterface { // MyClass 继承了 defaultMethod 的默认实现 // defaultMethod可以不用重写,也可以根据需要进行重写 public default void defaultMethod() { } } // 使用时 MyClass myClass = new MyClass(); myClass.defaultMethod(); // 调用默认方法
【接口冲突】:一个类实现了两个接口,而两个接口中定义了同名同参数的默认方法,则实现类在没有重写默认方法的情况下,会报错。因此实现类必须重写接口中定义的同名同参数的方法,相当于将两个都一起重写。
【类优先原则】:在Java
中,当子类同时继承父类并实现接口,且父类和接口中存在同名同参数的默认方法时,Java 遵循【类优先原则】,即父类的具体方法优先级高于接口的默认方法。
// 接口定义
interface MyInterface {
default void doSomething() {
System.out.println("Interface default method");
}
}
// 父类定义
class Parent {
public void doSomething() {
System.out.println("Parent class method");
}
}
// 子类继承父类并实现接口
class Child extends Parent implements MyInterface {
// 未重写 doSomething()
}
public class Main {
public static void main(String[] args) {
Child child = new Child();
child.doSomething(); // 输出:Parent class method
}
}
接口的作用如下:
-
面向接口调用的称为:接口调用者
-
面向接口实现的称为:接口实现者
-
调用者和实现者通过接口达到解耦合,也就是说调用者不需要关心具体的实现,实现者也不需要关心具体的调用者,双方都遵循规范,面向接口开发。
-
面向接口编程、面向抽象编程,可以降低代码的耦合度,提高代码的扩展力。
public class Computer { public void conn(HardDrive hardDrive) { System.out.println("硬盘连接成功"); hardDrive.read(); hardDrive.write(); } }
class HardDrive{ public void read() { System.out.println("硬盘开始读数据"); } public void write() { System.out.println("硬盘开始写数据"); } }
// 扩展,则需要改写Computer添加新的conn方法 class Printer{ public void read() { System.out.println("打印机开始打印"); } public void write() { System.out.println("打印机始写数据"); } }
// 测试 public static void main(String[] args) { HardDrive hardDrive = new HardDrive(); Computer computer = new Computer(); computer.conn(hardDrive); }
上述代码如果需要扩展就需要修改源代码,不符合
OCP
原则,这时候就可以使用接口来完成代码的解耦合:调用者是Computer
、实现者是HardDrive
和Printer
【扩展功能】。class HardDrive implments Usb{ public void read() { System.out.println("硬盘开始读数据"); } public void write() { System.out.println("硬盘开始写数据"); } }
class Printer implments Usb{ public void read() { System.out.println("打印机开始打印"); } public void write() { System.out.println("打印机始写数据"); } }
public interface Usb { void read(); void write(); }
public class Computer { public void conn(Usb usb) { usb.read(); usb.write(); } }
// 测试 public static void main(String[] args) { HardDrive hardDrive = new HardDrive(); Printer printer = new Printer(); Computer computer = new Computer(); computer.conn(hardDrive); computer.conn(printer); }
那么接口和抽象类应该如何进行选择呢?
- 抽象类主要适用于公共代码的提取。
- 接口主要用于功能的扩展,就比如一些类需要这个方法,另一些类不需要这个方法时,就可以将这个方法定义在接口中。
4.16 类与类之间的关系
类与类之间常见的关系有以下几种:
- 泛化关系:即继承关系,
is-a
- 实现关系:
is like a
- 关联关系:
has a
- 聚合关系:指一个类包含、合成或者拥有另一个类的实例,这个实例是可以独立存在的,表示整体与部分的关系,如一个一个教室有多个学生。
- 组合关系:组合关系是聚会关系的一种特殊情况,整体与部分的关系更加强烈。如一个人对应四个肢体,如果人没了肢体也没了。
- 依赖关系:依赖关系是一种临时性关系,当一个类使用另一个类的关系时,就会产生依赖关系。如果一个类的改变会影响另一个类的改变,那么这两个类就存在依赖关系。
在软件开发中,一般使用URL
类图来描述类与类之间的关系。常用工具如StartURL
,破解方法如下:
npm install -g asar
asar -V
cd "c:\Program Files\StarUML\resources"
// 反编译startUML
asar extract app.asar app
// src/engine/license-manager.js
async checkLicenseValidity() {
if (packageJSON.config.setappBuild) {
setStatus(this, true);
} else {
try {
const result = await this.validate();
setStatus(this, true);
} catch (err) {
const remains = this.checkEvaluationPeriod();
const isExpired = remains < 0;
// const result = await UnregisteredDialog.showDialog(remains); 将其注释
setStatus(this, true); // 改为true, 原来未false
if (isExpired) {
app.quit();
}
}
}
}
// src/app-context.js
if (!this.config.setappBuild) {
// 注释以下代码即可
// if (this.preferences.get("checkUpdate.checkUpdateOnStart")) {
// ipcRenderer.send("check-update");
// }
}
}
// 重新打包
asar pack app app.asar
4.17 java.lang.Object
java.lang.Object
是所有类的超类,java
中所有的类都实现了这个类中的方法。使用如下:
-
getClass()
:返回对象执行时的Class
实例,再通过getName()
就可以获得类的名称。Student stu = new Student(); System.out.println(stu.getClass().getName()); // per.java_code.Student
-
toString()
:将一个对象返回字符串形式,通常需要重写。输出对象时会默认调用该方法,且省略还可以避免空指针异常。 -
Class getClass()
:返回包含对象信息的类对象。 -
equals()
:比较两个对象是否相等,默认比较内存地址是否相同,通常需要重写。在Java
中,有两种比较对象的方式,==
是比较两个对象的内存地址是否相同,equals()
比较两个对象的实际内容是否相同。 -
int hashcode()
:返回对象的散列码,散列码可以是任意的整数,包括正数或负数。两个相等的对象要求返回相等的散列码。class Student extends Person { String name; int point; Student(String name, int point) { this.name = Objects.requireNonNullElse(name, "unknown"); this.point = point; } public boolean equals(Object obj) { if (obj == this) { return true; } if (obj == null || (this.getClass() != obj.getClass())) { return false; } Student stu = (Student)obj; if (this.point == stu.point) { return true; } return false; } public int hashcode() { return Objects.hash(this.point); } } public class package_demo { public static void main(String[] args) { // 比较两个人的成绩是否相同 Student stu = new Student("李明", 80); Student stu1 = new Student("布鲁斯", 80); System.out.println(stu.equals(stu1)); // true } }
-
hashCode
:返回一个对象的哈希值,通常作为在哈希表中查找对象的键值。Object
类默认实现是根据对象的内存地址生成一个哈希码【将内存地址转换为整数作为哈希值】。hashCode
方法是为了HashMap
、HashTable
等集合类进行优化而设置的,一边更快地查找和存储对象。 -
finalize
:当java
对象被回收时,由GC
自动调用对象的finalize
方法,在对象死之前做的事儿写在该方法中。 -
clone
:完成对象的拷贝,分为深拷贝和浅拷贝。只有实现了Cloneable
接口的对象才能被克隆。-
浅拷贝:从
Object
类继承的clone()
是protected
的,需在子类中重写为public
。调用super.clone()
实现默认的浅拷贝。浅拷贝仅复制对象本身,不复制其引用的对象,如数组和其它对象。public class Person implements Cloneable { private String name; private int age; // 构造方法、getter/setter 省略 @Override public Person clone() { try { return (Person) super.clone(); // 浅拷贝 } catch (CloneNotSupportedException e) { throw new AssertionError(); // 不会发生,因为已实现 Cloneable } } }
-
深拷贝:复制对象及其引用的所有对象,形成完全独立的副本。手动递归复制引用类型的字段即可实现。
public class DeepCopyDemo implements Cloneable { private int[] data = {1, 2, 3}; @Override public DeepCopyDemo clone() throws CloneNotSupportedException { DeepCopyDemo copy = (DeepCopyDemo) super.clone(); copy.data = data.clone(); // 复制数组 return copy; } public static void main(String[] args) throws CloneNotSupportedException { DeepCopyDemo obj1 = new DeepCopyDemo(); DeepCopyDemo obj2 = obj1.clone(); obj1.data[0] = 100; System.out.println(obj2.data[0]); // 输出 【副本未被修改】 } }
-
4.18 内部类
内部类也属于类成员之一,内部类就是定义在一个类中的类。当两个类的联系比较密切时,就可以使用内部类。
4.18.1 成员内部类
在成员内部类中,可以任意使用外部类的成员方法及变量,即使这些类成员被修饰为private
。
-
可以使用权限修饰符,如使用
private
修饰该成员内部类则该内部类只能在外部类中使用。 -
成员内部类中不能定义静态成员。
-
内部类对象必须依赖于外部类对象存在。
-
在外部类和外部类中的非静态方法中可以直接实例化内部类对象并使用。如果想要在外部类中的静态方法或其他类中使用内部类对象,则需要使用
外部类.内部类
的形式指定该对象的类型。 -
成员内部类中
this
的使用:成员内部类中使用this表示当前成员内部类对象,如果想要使用外部类的同名成员,则需要通过【外部类.this
.外部类成员】的方式访问外部类的同名成员。不同名成员直接通过成员名访问即可,如果是静态成员,则最好使用【外部类.静态成员】的方式访问。public class Car { private String brand; //汽车品牌 public Car(String brand) { this.brand = Objects.requireNonNullElse(brand, "unknown"); } class Engine { String model; // 发动机型号 public Engine (String model) { this.model = Objects.requireNonNullElse(model, "unknown"); } public void ignite() { // 点火方法 System.out.println("发动机" + this.model + "点火"); } } public void start() { // 汽车启动方法 System.out.println("启动" + this.brand); } public static void main(String[] args) { Car car = new Car("大众"); car.start(); Car.Engine engine = car.new Engine("EA211"); engine.ignite(); } }
4.18.2 局部内部类
局部内部类定义在方法体中,又称为方法内部类,局部内部类的作用域仅仅在定义它的方法或代码块中。
-
局部内部类可以直接访问外部类的所有成员属性和方法。
-
局部内部类不能添加权限修饰符,但是可以添加
final
修饰。 -
外部类与局部内部类成员重名,则采用就近原则,访问外部类同名成员使用【外部类.
this
.成员】。public class JubuInnerClass { // 1.外部类的属性 private String field01 = "外部类的私有属性field01"; public String field02 = "外部类的共有属性field02"; // 2.外部类的构造器 public JubuInnerClass() { } public JubuInnerClass(String field01, String field02) { this.field01 = field01; this.field02 = field02; } // 3.外部类的方法 public void method01(){ System.out.println("== 外部类的共有方法 : method01 "); } private void method02(){ System.out.println("== 外部类的私有方法 : method02 "); } // * 包含内部类的方法 public void method03(){ System.out.println("== 外部类的方法03 begin : 此方法包含一个局部内部类 == "); System.out.println(); // 下面开始编写 局部内部类的代码 class innerClass{ // 1.局部内部类 的 属性 private String field01_inner = "局部内部类的私有属性"; public String field02_inner = "局部内部类的共有属性"; private String field02 = "局部内部类的和外部类重名的属性"; // 2.局部内部类的 构造器 public innerClass() { } public innerClass(String field01_inner, String field02_inner, String field02) { this.field01_inner = field01_inner; this.field02_inner = field02_inner; this.field02 = field02; } //3.局部内部类的方法 public void method01_inner(){ System.out.println("--> 这是局部内部类的方法 method01_inner : begin <--"); System.out.println("直接访问外部类的私有成员 field01 : "+field01); System.out.println("直接访问外部类的方法method01()、method02 :"); method01(); method02(); System.out.println("--> 这是局部内部类的方法 method01_inner : end <--"); } public void method02_inner(){ System.out.println("--> 这是局部内部类的方法 method02_inner 演示内部类如何访问同名的外部类的属性field02 begin <--"); System.out.println("外部类的属性 field02 : "+JubuInnerClass.this.field02); System.out.println("--> 这是局部内部类的方法 method02_inner 演示内部类如何访问同名的外部类的属性field02 end <--"); } } // innerClass end // 外部类访问内部类 : 创建内部类的对象,通过对象进行访问 innerClass innerClass = new innerClass(); innerClass.method01_inner(); System.out.println(); innerClass.method02_inner(); System.out.println(); System.out.println("== 外部类的方法03 end : 此方法包含一个局部内部类 == "); } public static void main(String[] args) { //本代码演示使用内部类的情况 JubuInnerClass jubuInnerClass = new JubuInnerClass(); jubuInnerClass.method03(); } }
4.18.3 静态内部类
静态内部类即用static
关键字修饰的成员内部类,可以把静态内部类类比静态方法。
- 可以直接访问外部类的静态成员,如果要访问外部类的非静态成员,必须创建外部类对象。
- 可以添加任意权限修饰符。
- 外部类访问静态内部类中的成员
- 静态成员:【静态内部类.静态成员】
- 非静态成员:【对象.非静态成员】
- 其他类访问静态内部类成员
- 静态成员:【外部类.静态内部类.静态成员】
- 非静态成员:创建【外部类.静态内部类】类型的对象。
4.18.4 匿名内部类
匿名内部类定义在方法之中,主要是在方法的形参上使用,这个类没有名字,一般默认继承一个类或者一个接口。匿名内部类适合创建那种只需要一次使用的类,匿名内部类写出来就会产生一个该类/接口的子类对象。
package per.java_code;
abstract class Device {
private String name;
public abstract double getPrice();
public Device() {
}
public Device(String name) {
this.name = name;
}
// 此处省略了name的setter和getter方法
public void setName(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
}
public class AnonymousInner {
public void test(Device d) {
System.out.println("购买了一个" + d.getName()
+ ",花掉了" + d.getPrice());
}
public static void main(String[] args) {
AnonymousInner ai = new AnonymousInner();
// 调用有参数的构造器创建Device匿名实现类的对象
ai.test(new Device("电子示波器") {
@Override
public double getPrice() {
return 67.8;
}
});
// 调用无参数的构造器创建Device匿名实现类的对象
Device d = new Device() {
// 初始化块
{
System.out.println("匿名内部类的初始化块...");
}
// 实现抽象方法
@Override
public double getPrice() {
return 56.2;
}
// 重写父类的实例方法
@Override
public String getName() {
return "键盘";
}
};
ai.test(d);
}
}