IDEA快捷键
- 创建main函数
psvm + Tab
- 创建printf函数
souf + Tab
- 创建println函数
sout + Tab
- 代码格式化
Ctrl + Alt + L
1 Java基础知识
1 Java特性
Java语言是简单的
- Java语言的语法与C语言和C++语言很接近,使得大多数程序员很容易学习和使用。
- Java丢弃了C++中很少使用的、很难理解的、令人迷惑的那些特性,如操作符重载、多继承、自动的强制类型转换。
- 特别地,Java语言不使用指针,而是引用,并提供了自动分配和回收内存空间的机制,使得程序员不必为内存管理而担忧。
Java语言是面向对象的
- Java语言提供类、接口和继承等面向对象的特性,为了简单起见,只支持类之间的单继承,但支持接口之间的多继承,并支持类与接口之间的实现机制(关键字为
implements
)。 - Java语言全面支持动态绑定,而C++语言只对虚函数使用动态绑定。
- 总之,Java语言是一个纯的面向对象程序设计语言。
- Java语言提供类、接口和继承等面向对象的特性,为了简单起见,只支持类之间的单继承,但支持接口之间的多继承,并支持类与接口之间的实现机制(关键字为
Java语言是分布式的
- Java 语言支持Internet应用的开发,在基本的Java应用编程接口中有一个网络应用编程接口(java net),它提供了用于网络应用编程的类库,包括URL、URLConnection、Socket、ServerSocket等。
- Java的RMI(远程方法激活)机制也是开发分布式应用的重要手段。
Java语言是健壮的
- Java的强类型机制、异常处理、垃圾的自动收集等是Java程序健壮性的重要保证。
- 对指针的丢弃是Java的明智选择。
- Java的安全检查机制使得Java更具健壮性。
Java语言是安全的
- Java通常被用在网络环境中,为此,Java提供了一个安全机制以防恶意代码的攻击。
- 除了Java语言具有的许多安全特性以外,Java对通过网络下载的类具有一个安全防范机制(类 ClassLoader),如分配不同的名字空间以防替代本地的同名类、字节代码检查,并提供安全管理机制(类 SecurityManager)让 Java应用设置安全哨兵。
Java语言是体系结构中立的:
- Java程序(后缀为 java的文件)在Java平台上被编译为体系结构中立的字节码格式(后缀为class的文件),然后可以在实现这个Java平台的任何系统中运行。
- 这种途径适合于异构的网络环境和软件的分发。
Java语言是可移植的
- 这种可移植性来源于体系结构中立性
- Java还严格规定了各个基本数据类型的长度,使各个平台的基本数据类型长度统一。
- Java系统本身也具有很强的可移植性,Java编译器是用Java实现的,Java的运行环境是用ANSI C实现的。
Java语言是解释型的:
- 如前所述,Java程序在Java平台上被编译为字节码格式,然后可以在实现这个Java平台的任何系统中运行。
- 在运行时,Java平台中的Java解释器对这些字节码进行解释执行,执行过程中需要的类在联接阶段被载入到运行环境中。
- Java源程序(解释型)与编译型源程序(如C/C++)运行的区别
Java是高性能的:
- 与那些解释型的高级脚本语言相比,Java的确是高性能的。
- 事实上,Java的运行速度随着JIT(Just-In-Time)编译器技术的发展越来越接近于C++。
Java语言是多线程的:
- 在Java语言中,线程是一种特殊的对象,它必须由Thread 类或其子(孙)类来创建。
- 通常有两种方法来创建线程:其一,使用型构为Thread(Runnable) 的构造子类将一个实现了Runnable接口的对象包装成一个线程,其二,从Thread类派生出子类并重写run方法,使用该子类创建的对象即为线程。
- 值得注意的是Thread类已经实现了Runnable接口,因此,任何一个线程均有它的run方法,而run方法中包含了线程所要运行的代码。
- 线程的活动由一组方法来控制。Java语言支持多个线程的同时执行,并提供多线程之间的同步机制(关键字为
synchronized
)。
Java语言是动态的:
- Java 语言的设计目标之一是适应于动态变化的环境。
- Java程序需要的类能够动态地被载入到运行环境,也可以通过网络来载入所需要的类,这也有利于软件的升级。
- 另外,Java 中的类有一个运行时刻的表示,能进行运行时刻的类型检查。
2 Java术语
- Java SE(Java Platform, Standard Edition)
Java SE是Java的标准版本,是JDK的具体实现,一般说的Java就是指Java SE。
- Java EE(Java Platform,Enterprise Edition)
Java EE是Java的企业版本,是在Java SE的基础上发展出来的一套接口规范
- Java ME(Java Platform,Micro Edition)
Java SE的微型版本,用于嵌入式设备
- JDK(Java Development Kit)
JDK包括Java程序设计语言、Java虚拟机和Java类库,是支持Java程序开发的最小环境
- JRE(Java Runtime Environment)
JRE包括Java虚拟机和Java类库中的Java SE API子集,是支持Java运行的标准环境
- Java版本号
- Java版本号实际上指的是JDK的版本号
- JDK从1.5版本开始,发行版本号启用了JDK 5这样的新命名方法。
- 从JDK 10开始,JDK的开发版本号放弃了之前的1.x的命名形式
3 Java环境变量配置
- 环境变量定义
环境变量是在操作系统中一个具有特定名字的对象,它包含了一个或者多个应用程序所将使用到的信息。
- 我们通过一个具体的应用场景来理解:毫无疑问,想要运行一个软件,我们必须知道其启动程序程序的位置,同样的在命令行输入一个命令,计算机需要知道命令所在位置,否则会提示没有此命令。
- 当我们输入“notepad”打开记事本程序时,操作系统会去环境变量已保存的路径中查找是否存在notepad程序。
- 如果没有配置好环境变量,我们就必须告诉操作系统绝对路径:”C:\Windows\notepad”
- 环境变量配置
在初次安装Java后,为了更加方便的使用,需要配置环境变量。或者说安装了新版本的JDK后,使用java -version查看发现还是之前的版本,这也是环境变量配置的原因。
通常情况我们需要配置以下三个环境变量:
- 新建变量
**JAVA_HOME**
:指向JDK的安装目录,作用是一些基于Java开发的工具会用到,比如tomcat、Eclipse,如果不用这些工具不需要配置。 - 追加变量
**PATH**
:指向JDK安装目录下的bin目录,作用是指定命令搜索路径,bin目录下有编译、启动(javac/java)等命令。为了任何目录位置下都可以直接输入命令,而不用输入长长的路径了。如果配置了JAVA _HOME ,直接把%JAVA_HOME%/bin追加到PATH中即可。 - 新建变量
**CLASSPATH**
:在于告诉Java执行环境,在哪些目录下可以找到我们所要执行的Java程序所需要的类或者包。不过在JDK1.5之后的版本完全可以不用设置CLASSPATH环境变量就能正常运行程序。
4 包
Java允许使用包(package)将类组织在一个集合中(包可以说是类的集合),借助包可以方便地组织自己的代码,并将自己的代码与别人提供的代码库分开管理。
为什么使用包
- 使用包的主要原因是确保类名的唯一性(防止命名冲突)。
如同文件夹一样,包也采用了树形目录的存储方式。同一个包中的类名字必须是不同的,不同的包中的类的名字可以是相同的。当同时调用两个不同包中相同类名的类时,应该加上包名加以区别。因此,包可以避免名字冲突。
- 把功能相似或相关的类或接口组织在同一个包中,方便类的查找和使用。
- 包也限定了访问权限,拥有包访问权限的类才能访问某个包中的类。
- 包声明
- 在每个.java文件的第一行需要使用
package
关键字标注所属包
- 在每个.java文件的第一行需要使用
如MyClass.java文件属于pers.first包,那么MyClass.java文件的内容应该为
package pers.first;
public class MyClass {
...
}
- 每个源文件只能有一个包声明
- 如果一个源文件中没有使用包声明,那么其中的类,函数,枚举,注释等将被放在一个无名的包(unnamed package)中(即使它放在某个包的目录中)
- 包的命名
为了保证包名的绝对唯一性,要用一个因特网域名(显然是唯一的)以逆序的形式作为包名,然后对于不同的工程使用不同的子包。
- 如根据域名
horstmann.com
,得到包名com.horstmann
,然后可以再追加一个工程名,即可得包名**com.horstmann.corejava**
- 包名通常以全部小写字母来命名
类的存储路径与包名
在本地编译代码时,源文件应该放到与完整包名匹配的子目录中(编译器会将类文件.class放到相同目录),即类存储在文件系统的子目录中,类的路径必须与包名匹配。例如:
- A.B.C包中的类a应该放到子目录A\B\C中,即
A\B\C\a.java
- A.B包中的类b应该放到子目录A\B中,即
A\B\b.java
- A.B.C包中的类a应该放到子目录A\B\C中,即
编译器在编译源文件的时候不检查目录结构,如果一个源文件开头为
packge com.my
,但是它却不在子目录com\my
中,而且它不依赖于其他包,那么也可以进行编译。但最终程序无法运行,除非将所有类文件移到正确的位置上,因为包与目录不匹配时,虚拟机找不到类。
类的完全限定名
每个类有其“完全限定”名(全名),保证其唯一性,如java.util
包内的Date
类,其完全限定名就是java.util.Date
- 嵌套的包之间没有任何关系
如java.util
包与java.util.jar
包毫无关系,两个包都包含了多个类,都是独立的类集合。
- 常用的java内置包
java.lang
包含基础的类java.io
包含输入输出功能的函数
5 类的导入
- 一个类可以访问所属包中的所有类,以及其他包中的公共类(public class)
- 同一个包内可以直接使用类名访问
- 访问另一个包中的公共类则有两种方式
使用类的完全限定名,不需要import
相应包
- 如`java.time.LocalDate today = java.time.LocalDate.now();`
使用**import**
语句导入包中一个特定的类或者所有类。
- 使用`import`语句后,就不必使用类的完全限定名了
import
语句应位于源文件的顶部,但位于package
语句的后面。在包名后面加上*,可以导入包中的所有类
如import java.time.*
- 这种方式语法简单,对代码的规模也没有任何负面影响。
- 只能使用
*****
导入一个包中的所有类,而不能使用import java.*
或import java.*.*
导入以java为前缀的所有包的所有类。 - 使用
*
导入包很方便,但在发生命名冲突时需要注意
- 静态导入
**import static**
语句允许导入静态方法和静态字段,
如import static java.lang.Math.*
- 静态导入的方式可以直接使用类的静态方法和静态字段,而不必加类名前缀
如直接使用sqrt(x)
而不是Math.sqrt(x)
- 有时使用静态导入会使代码看起来不太清晰。
2 Java的基本程序设计结构
2.1 我的第一个Java程序
public class Test {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
- 类是构建所有Java应用程序的构建块。
- Java应用程序的全部内容都必须放置在类中。
即Java中的所有函数都是某个类的方法,所以main()
方法必须有一个外壳类(shell)
2.2 代码规则
在Java中,每个句子必须用分号结束。
回车不是语句结束的标志,因此,可以将一条语句写在多行上。
注意:回车符不在一个完整的单词中间,只能在符号的前面或后面
- 点号
.
用于调用类/对象的方法
2.3 注释
- Java三种注释方式
**//**
这种方式,其注释内容从//
开始到本行结尾
**/* … */**
这种注释界定符可以将一段比较长的多行注释括起来
- 该种注释**不能嵌套**
**/** … */**
这种注释方式可以用来自动地生成文档
2.4 变量
- 与所有其他语言一样,Java也使用变量来存储值。变量就是申请内存来存储值,也就是说,当创建变量的时候,需要在内存中申请空间,内存管理系统根据变量的类型为变量分配存储空间,分配的空间只能用来储存该类型数据。
- Java有两大类数据类型
- 内置数据类型(基本数据类型)
- 引用数据类型
- 主要有如下三种类型的变量
- 局部变量
- Java有两大类数据类型
类的方法中的变量
- **类变量(静态变量)**
独立于方法之外的变量,用static
修饰。
- **实例变量(非静态变量)**
独立于方法之外的变量,不过没有static
修饰。
可以在一行中声明多个变量,不过建议每行声明一个变量,可以提高程序的可读性。
变量的命名
- 变量名必须以字母开头,同时建议以小写字母开头
- 由多个单词组成的变量名,常采用驼峰命名法,如
hireDay
- 许多程序员将变量名命名为类型名的小写,或者在变量名前加前缀“a”
Box box;
Box abox;
变量的声明和初始化
- 可以将变量的声明和初始化放在同一行,如
int vacationDays = 12;
- 变量的声明可以放在代码中的任何地方
- 可以将变量的声明和初始化放在同一行,如
局部变量
局部变量各概念
局部变量的声明与可见性
- 局部变量声明在类的方法或者方法的语句块中;
- 局部变量只在声明它的方法、构造方法或者语句块中可见;
生存周期
局部变量在方法、构造方法、或者语句块被执行的时候创建,当它们执行完成后,变量将会被销毁;
访问修饰符不能用于局部变量;
存储位置
局部变量是在栈上分配的。
- 局部变量没有默认值(包括对象和基本数据类型,都没有默认值),所以局部变量被声明后,必须经过初始化,才可以使用。
语句块内局部变量的可见性 ```java public class MyClass { public static void main(String[] args) throws IOException {
for(int i = 0;i<1;i++) {
int x = 0; //仅声明此变量的大括号内可见
}
if(true){
int y = 0; ////仅声明此变量的大括号内可见
}
if(true)
int z = 0; //报错:Declaration not allowed here
System.out.println(x); //报错:Cannot resolve symbol 'x'
System.out.println(y); //报错:Cannot resolve symbol 'y'
}
}
从上例中我们可以看到
- **在如**`**if**`**、**`**for**`**、**`**while**`**等语句块中声明的变量,语句块外不可见,即使是在同一个方法中**
- 如果想要在某个函数体内声明变量,必须加上大括号
- **var关键字**
- 对于局部变量,如果可以从变量的初始值推导出它们的类型,那么可以用`var`关键字声明局部变量,而无需指定类型。
```java
var vacationDays = 12; //vacationDays is an int
var greeting = "Hello"; //greeting is a String
var date = LocalDate.now(); //date is a LocalDate
- var关键字只能用于方法中的局部变量,方法的参数和类的实例字段的类型必须声明。
实例变量
实例变量声明在一个类中,方法之外;
生存周期
实例变量在对象创建的时候创建,在对象被销毁的时候销毁;
实例变量对于类中的方法、构造方法或者语句块都是可见的。
一般情况下应该把实例变量设为
private
或protected
实例变量具有默认值(基础类型和对象都有默认值)。
实例变量可以直接通过变量名访问,但是在静态方法中不能直接访问
- 静态方法访问实例字段
在静态方法中新建对象,然后通过对象名.变量名
的方式可以访问实例字段
public class MyClass {
private int a = 3;
public static void main(String[] args) {
MyClass myClass = new MyClass();
System.out.println(myClass.a); //静态方法访问实例字段,输出3
}
}
类变量(静态变量)
在非访问控制符一节也有关于类变量的介绍
类变量也称为静态变量,在类中以
static
关键字声明,声明在一个类中,方法之外;无论一个类创建了多少个对象,类只拥有类变量的一份拷贝。
静态变量除了被声明为常量外很少使用
静态常量是指声明为final
和static
类型的变量,很少单独使用**static**
声明变量。
静态变量储存在静态存储区。
生存周期
静态变量在第一次被访问时创建,在程序结束时销毁。
2.5 基本数据类型
Java是一种强类型语言,必须为每一个变量声明一种类型
Java共有8种基本类型
- 4种整形类型
int、short、long、byte
- 2种浮点类型
float、double
- 1种字符类型
char
- 1种布尔值类型
boolean
2.5.1 整型
4种整型的各项数据表 | 类型 | 存储需求 | 取值范围 | | —- | —- | —- | | int | 4字节 | -2 147 483 64 ~ 2 147 483 647(-2.1×10^-9 ~ 2.1×10^9) | | short | 2字节 | -32 768 ~ 32 767(-2^15 ~ 2^15-1) | | long | 8字节 | 9.2×10-18 ~ 9.2×1018(-2^63 ~ 2^63-1) | | byte | 1字节 | -128 ~ 127(-2^7 ~ 2^7-1) |
Java为了可移植性,数据类型的取值范围必须固定,与运行Java代码的机器无关
C和C++中,
int
和long
等类型的字节大小与处理器的位数有关实际上对于基本类型的取值范围(除了
boolean
类型),我们无需强制去记忆,因为它们的值都已经以常量的形式定义在对应的包装类中了。如int
的包装类Integer
public class PrimitiveTypeTest {
public static void main(String[] args) {
// 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);
}
}
输出结果:
基本类型:int 二进制位数:32
包装类:java.lang.Integer
最小值:Integer.MIN_VALUE=-2147483648
最大值:Integer.MAX_VALUE=2147483647
byte
与short
的使用byte
类型用在大型数组中节约空间,主要用于代替整数,因为byte
变量占用的空间只有int
类型的四分之一;short
数据类型也可以像byte
那样节省空间。一个short
变量是int
型变量所占空间的二分之一;
Java没有任何无符号(
**unsigned**
)形式的数据类型前缀
前缀用于
- 十六进制:
0x
或0X
- 八进制:
0
(八进制表示法容易混淆,所以最好不要使用八进制常数) - 二进制:
0b
或0B
各进制转换成字符串后仍以十进制表示
- 后缀
一个数后面加上后缀l或L表示长整型数,如100000000L
,但是这个后缀在给long类型变量赋值时不是必须的。
long x = 100; //可以通过编译
- 可以在数字中间加下划线,使得某些较长的数字更易读,如
- 用
1_000_000
表示100万 - 用
0b0010_1000
表示32
- 用
2.5.2 浮点型
- Java的浮点型数据遵循IEEE754规范,
一个浮点数的所有位共分为三部分:符号位,指数位,尾数位
类型 | 存储需求 | 符号位(S) | 指数位(E) | 尾数位(M) |
---|---|---|---|---|
float | 4字节16bit | 1bit | 8bit | 23bit |
double | 8字节64bit | 1bit | 11bit | 52bit |
- 表示范围
- 负指数决定了浮点数所能表达的绝对值最小的非零数;
正指数决定了浮点数所能表达的绝对值最大的数
由此指数的位数决定了浮点数的取值范围。
- 指数是补码形式,对于
float
来说,真实指数为:E-127 float
:-2128 ~ 2128,即±3.4×1038double
:-21024 ~ 21024,即±1.79×10308
- 精度
- 浮点数的精度是由尾数的位数来决定的。
- 浮点数在内存中是按科学计数法来存储的,其整数部分始终是一个隐含着的“1”,由于它是不变的,故不能对精度造成影响。
float
223 = 8388608,一共七位,由于最左为1的一位省略了,这意味着最多能表示8位数。
2*8388608 = 16777216 ,8位有效数字,但绝对能保证的有7位,即float
的精度为7~8位有效数字
double
252 = 4503599627370496,一共16位,同理,double
的精度为16~17位。
- 2种浮点型的各种数据表 | 类型 | 存储需求 | 取值范围 | 精度/有效位 | | —- | —- | —- | —- | | float | 4字节 | -2128 ~ 2128,即±3.4×1038 | 7 ~ 8位 | | double | 8字节 | -21024 ~ 21024,即±1.79×10308 | 16 ~ 17位 |
- 后缀
在浮点数后面加上f或F可以表示float
类型浮点数,没有后缀的浮点数值默认为double
类型。
为float类型变量赋值时,浮点数必须加上f或F后缀,这一点与long
型整数不同
float f = 3.14f; //通过编译
float f = 3.14; //错误,无法通过编译
底层的二进制数不能精确表示所有的小数,有时会产生一些意想不到的问题,如:
0.99999999f==1f //true
0.9f==1f //false
因此,不能使用
**==**
判断两个浮点数是否相等由于
float
类型的精度较小,只有很少情况适合使用float
数据,使用浮点数通常使用**double**
类型。浮点数值不适用与无法接受舍入误差的计算,如果在数值计算中不允许有任何舍入误差,应该使用
**BigDecimal类。**
2.5.3 字符型
使用单引号
''
括起来的值是char
型值char类型用于表示单个字符和Unicode字符
尽量不要在程序中使用
**char**
型数据,表示字符时请使用String
类。
2.5.4 布尔型
boolean
类型有两个值:false
和true
,用来判定逻辑条件。整型值和
boolean
值之间不能进行相互转换。
2.5.5 各类型数据的默认值
如果只声明了一个变量,而没有对其显式赋值,那么变量将根据其类型而被赋予默认值
注意,这里只限于类的实例变量或类变量;局部变量没有默认值,不初始化就不能使用(除了局部数组变量,其在任何位置都可以被赋予默认值)。 ```java public class MyClass {
private static int a;
public static void main(String[] args) { int b; System.out.println(a); //编译通过 System.out.println(b); //因为b没有被初始化,编译不通过 }
}
- **各类型对应的默认值表**
| **数据类型** | **默认值** |
| --- | --- |
| **byte** | 0 |
| **short** | 0 |
| **int** | 0 |
| **long** | 0L |
| **float** | 0.0f |
| **double** | 0.0 |
| **char** | 'u0000' |
| **boolean** | false |
| **any object** | null |
<a name="e91c060c0c25d5ac53569620cb7b9641"></a>
### 2.5.6 包装类
- 一般地,当需要使用数字的时候,我们通常使用内基本据类型,如:`byte`、`int`、`long`、`double`等。
**在实际开发过程中,我们经常会遇到需要使用对象,而不是内置数据类型的情形**。为了解决这个问题,**Java语言为每一个内置数据类型提供了对应的包装类。**
- 数字类基本类型都是抽象类`Number`的子类,`boolean`和`char`则不然,具体看下图
![](https://cdn.nlark.com/yuque/0/2021/png/1169704/1615286240563-4363283f-b235-4aa0-97cf-066aaa338506.png#crop=0&crop=0&crop=1&crop=1&height=235&id=a9dr0&originHeight=235&originWidth=545&originalType=binary&ratio=1&rotation=0&showTitle=false&size=0&status=done&style=none&title=&width=545)
- 由编译器特别支持的包装称为装箱,所以当内置数据类型被当作对象使用的时候,编译器会把内置类型**装箱**为包装类。相似的,编译器也可以把一个对象**拆箱**为内置类型。
- **实例**
```java
public class Test{
public static void main(String[] args){
Integer x = 5; //当x被赋为整型值时,由于x是一个对象,所以编译器要对x进行装箱
x = x + 10; //为了使x能进行加运算,所以要对x进行拆箱
System.out.println(x); //输出15
}
}
- 由于装箱与拆箱操作的存在,包装类和基本数据类型可以混用
如double Math.abs(double d)
明确要求参数类型是基本数据类型,但也可以使用Double
实例作为参数
Double d = -0.05;
d = Math.abs(d); //编译通过,经历了拆箱:Math.abs(d)和装箱:d = 0.05
- 包装类与基本类型比较
包装类变量在与基本数据类型变量比较时,包装类会自动拆拆箱为基本类型,然后进行比较,实际上就是两个基本类型变量进行比较。
**Integer**
类型变量引用的地址Java 会对[-128, 127]范围内的整数进行缓存,所以当定义两个
Integer
变量初始化值位于[-128, 127]之间时,两个Integer
变量使用了同一地址:Integer a=123;
Integer b=123;
System.out.println(a==b); //out: true
System.out.println(a.equals(b));//out: true
当两个 Integer 变量的数值超出[-128, 127]范围时,
Integer
变量使用了不同地址:Integer a=1230;
Integer b=1230;
System.out.println(a==b); //out: false
System.out.println(a.equals(b));//out: true
###Number类(java.lang包)
- Number类是抽象类,方法在类中并没有实现,而是由各种包装类实现的。
- 这一节还包括一些包装类自己实现的方法。
动态方法**number**
指代任意数字基本型的包装类实例,如**Integer number;**
xxx number.xxxValue()
将number转换为xxx数据类型的值并返回,xxx可以是6种数字基本类型
Integer a = 10;
System.out.println(a.doubleValue()); //输出10.0
String number.toString()
用于将数字转为字符串
- 重写了Object类的
toString()
方法,直接返回number的字符串形式 - 也可以使用String类的静态方法
String.valueOf()
```java Integer i = 857; System.out.println(i.toString().equals(“857”)); //out: true
Double d = 0.556; System.out.println(d.toString().equals(“0.556”)); //out: true
**静态方法**<br />`**Number**`**指代任意数字包装类,如**`**Integer**`
- `Short Short.valueOf(short i)`
- `Integer Integer.valueOf(int i)`
- `Long Long.valueOf(long i)`
- `Float Float.valueOf(float i)`
- `Double Double.valueOf(double i)`
- `Byte Byte.valueOf(byte i)`
- `**Number Number.valueOf(String s)**`
这几个方法是包装类的**静态工厂方法(包装类构造方法已弃用)**
- 前6个方法可以将基本数据类型转换为相对应的包装类(不太常用,一般直接使用`=`号赋值)
- 无法将参数转换为非对应的包装类,如`Integer.valueOf(0.0)`无法通过编译)
- **最后一个函数最常用,用于将字符串转为字面数值。**
- 这里所有6个数字包装类都可以接受并转换字符串参数
```java
Double d = Double.valueOf(6.0); //等价于Double d = 6.0
Double d = Double.valueOf("100.99"); //d == 100.99
Integer i = Integer.valueOf("9875"); //i == 9875
- 参数为字符串时,实际上调用的是
parseXxx()
方法,如Integer.valueOf(String str)
的源码为public static Integer valueOf(String s) throws NumberFormatException {
return Integer.valueOf(parseInt(s, 10));
}
**number Number.parseNumber(String s)**
将字符串解析为数字
int x =Integer.parseInt("9"); //x == 9
double c = Double.parseDouble("5"); //c == 5.0
###Character类API(java.lang包)
静态方法
boolean Character.isLetter(char ch)
如果字符为字母,则返回true
;否则返回false
。
boolean Character.isDigit(char ch)
如果字符为字母,则返回true
;否则返回false
。
boolean Character.isWhitespace(char ch)
如果字符为空白字符,则返回true
;否则返回false
。
boolean Character.isUpperCase(char ch)
/boolean Character.isLowerCase(char ch)
如果字符为大写字母/小写字母,则返回true
;否则返回false
。
char Character.toUpperCase(char ch)
/char Character.toLowerCase(char ch)
返回转换后字符的大写/小写形式,如果有的话;否则返回字符本身。
2.6 数学问题
2.6.1 运算符
- 计算机的最基本用途之一就是执行数学运算,作为一门计算机语言,Java也提供了一套丰富的运算符来操纵变量。我们可以把运算符分成以下几组:
- 算术运算符
- 关系运算符
- 位运算符
- 逻辑运算符
- 赋值运算符
- 其他运算符
算术运算符
算术运算符用在数学表达式中,它们的作用和在数学中的作用一样。下表列出了所有的算术运算符。
表格中的实例假设整数变量A的值为10,变量B的值为20:
运算符 | 描述 | 例子 |
---|---|---|
+ | 加法 - 相加运算符两侧的值 | A + B 等于 30 |
- | 减法 - 左操作数减去右操作数 | A – B 等于 -10 |
* | 乘法 - 相乘操作符两侧的值 | A * B等于200 |
/ | 除法 - 左操作数除以右操作数 | B / A等于2 |
% | 取余 - 左操作数除以右操作数的余数 | B%A等于0 |
++ | 自增: 操作数的值增加1 | B++ 或 ++B 等于 21 |
— | 自减: 操作数的值减少1 | B— 或 —B 等于 19 |
关系运算符
下表为Java支持的关系运算符
表格中的实例整数变量A的值为10,变量B的值为20:
运算符 | 描述 | 例子 |
---|---|---|
== | 检查如果两个操作数的值是否相等,如果相等则条件为真。 | (A == B)为假。 |
!= | 检查如果两个操作数的值是否相等,如果值不相等则条件为真。 | (A != B) 为真。 |
> | 检查左操作数的值是否大于右操作数的值,如果是那么条件为真。 | (A > B)为假。 |
< | 检查左操作数的值是否小于右操作数的值,如果是那么条件为真。 | (A < B)为真。 |
>= | 检查左操作数的值是否大于或等于右操作数的值,如果是那么条件为真。 | (A >= B)为假。 |
<= | 检查左操作数的值是否小于或等于右操作数的值,如果是那么条件为真。 | (A <= B)为真。 |
位运算符
位运算符作用在所有的位上,并且按位运算。假设a = 60,b = 13;它们的二进制格式表示将如下:
A = 0011 1100
B = 0000 1101
-----------------
A&B = 0000 1100
A | B = 0011 1101
A ^ B = 0011 0001
~A= 1100 0011
下表列出了位运算符的基本运算,假设整数变量 A 的值为 60 和变量 B 的值为 13:
运算符 | 描述 | 例子 |
---|---|---|
& | 按位与:如果相对应位都是1,则结果为1,否则为0 | (A&B)得到12,即0000 1100 |
| | 按位或:如果相对应位都是 0,则结果为 0,否则为 1 | (A | B)得到61,即 0011 1101 |
^ | 按位异或:如果相对应位值相同,则结果为0,否则为1 | (A ^ B)得到49,即 0011 0001 |
〜 | 按位取反:翻转操作数的每一位,即0变成1,1变成0。 | (〜A)得到-61,即1100 0011 |
<< | 按位左移运算符。左操作数按位左移右操作数指定的位数。 低位补零 |
A << 2得到240,即 1111 0000 |
>> | 按位右移运算符。左操作数按位右移右操作数指定的位数。 高位补符号位 |
A >> 2得到15即 1111 |
>>> | 按位右移运算符。左操作数按位右移右操作数指定的位数。 高位补零,没有<<<运算符 |
A>>>2得到15即0000 1111 |
逻辑运算符
下表列出了逻辑运算符的基本运算,假设布尔变量A为真,变量B为假
运算符 | 描述 | 例子 |
---|---|---|
&& | 称为逻辑与运算符。当且仅当两个操作数都为真,条件才为真。 | (A && B)为假。 |
| | | 称为逻辑或操作符。如果任何两个操作数任何一个为真,条件为真。 | (A | | B)为真。 |
! | 称为逻辑非运算符。用来反转操作数的逻辑状态。如果条件为true,则逻辑非运算符将得到false。 | !(A && B)为真。 |
赋值运算符
下面是Java语言支持的赋值运算符:
运算符 | 描述 | 例子 |
---|---|---|
= | 简单的赋值运算符,将右操作数的值赋给左侧操作数 | C = A + B将把A + B得到的值赋给C |
+= | 加和赋值操作符,它把左操作数和右操作数相加赋值给左操作数 | C += A等价于C = C + A |
-= | 减和赋值操作符,它把左操作数和右操作数相减赋值给左操作数 | C -= A等价于C = C - A |
*= | 乘和赋值操作符,它把左操作数和右操作数相乘赋值给左操作数 | C = A等价于C = C A |
/= | 除和赋值操作符,它把左操作数和右操作数相除赋值给左操作数 | C /= A,C与A同类型时等价于C = C / A |
%= | 取模和赋值操作符,它把左操作数和右操作数取模后赋值给左操作数 | C%= A等价于C = C%A |
<<= | 左移位赋值运算符 | C <<= 2等价于C = C << 2 |
>>= | 右移位赋值运算符 | C >>= 2等价于C = C >> 2 |
&= | 按位与赋值运算符 | C&= 2等价于C = C&2 |
^= | 按位异或赋值操作符 | C ^= 2等价于C = C ^ 2 |
|= | 按位或赋值操作符 | C |= 2等价于C = C | 2 |
条件运算符(?:)
条件运算符也被称为三元运算符。该运算符有3个操作数,并且需要判断布尔表达式的值。
该运算符的主要是决定哪个值应该赋值给变量。
- 语法格式:
variable x = (expression) ? value if true : value if false
instanceof 运算符
该运算符用于操作对象实例,检查该对象是否是一个特定类型(类或接口)。
instanceof
运算符使用格式如下:(对象) instanceof (类/接口名)
如果运算符左侧变量所指的对象,是操作符右侧类或接口(class/interface)的一个对象,那么结果为真。
String name = "James";
boolean result = name instanceof String; // 由于name是String类型,所以返回true
如果被比较的对象兼容于右侧类型,该运算符仍然返回true。 ```java class Vehicle {}
public class Car extends Vehicle { public static void main(String[] args){ Vehicle a = new Car(); boolean result = a instanceof Car; System.out.println(result); //输出true } }
**运算符优先级**
- 当多个运算符出现在一个表达式中,谁先谁后呢?这就涉及到运算符的优先级别的问题。在一个多运算符的表达式中,运算符优先级不同会导致最后得出的结果差别甚大。
- **下表列出了运算符的优先级和结合性**
| **优先级** | **运算符类别** | **运算符** | **结合性** |
| --- | --- | --- | --- |
| 1 | 后缀 | ()、[]、.(点操作符) | 左到右 |
| 2 | 一元 | +(正号)、-(符号)、!、~、++、-- | 右到左 |
| 3 | 乘性 | *、/、% | 左到右 |
| 4 | 加性 | +、- | 左到右 |
| 5 | 移位 | >>、<<、>>> | 左到右 |
| 6 | 关系、类型 | >、>=、<、<=、instanceof | 左到右 |
| 7 | 相等 | ==、!= | 左到右 |
| 8 | 按位与 | & | 左到右 |
| 9 | 按位异或 | ^ | 左到右 |
| 10 | 按位或 | | | 左到右 |
| 11 | 逻辑与 | && | 左到右 |
| 12 | 逻辑或 | || | 左到右 |
| 13 | 条件 | ?: | 右到左 |
| 14 | 赋值 | =、+=、-=、*=、/=、%=、 >>=、<<=、&=、 ^=、|= | 右到左 |
<a name="340f35056bdd25994b73f1659725a6f7"></a>
### 2.6.2 自动类型转换
- 各基本数据类型的数据(除`boolean`类型)可以进行混合运算。
**运算中,不同类型的数据先转化为同一类型,然后进行运算**。
- **操作数类型的转换优先级为**
`byte`、`short`、`char` → `int` → `long` → `float` → `double`
- 如`double`和`int`运算时,`int`会自动转换为`double`类型。
- **自动类型转换的内在规则**
- 必须满足**转换前的数据类型的位数要低于转换后的数据类型**,例如: `short`数据类型的位数为16位,就可以自动转换位数为32的`int`类型,同样`float`数据类型的位数为32,可以自动转换为64位的`double`类型。
- 这样可以**保证转换后不会丢失任何信息**
<a name="a1610b108d50f63fa66acc5a23836219"></a>
### 2.6.3 强制类型转换
- 我们在进行运算时,`int`类型的值有可能会自动地转换为`double`类型,这种自动转换并不会丢失信息。
但有时,我们也需要将`double`转换为`int`,这种转换有可能会丢失一些信息。<br />可能丢失信息的,显式地类型转换要通过**强制类型转换(cast)**来完成。
- **能够强制类型转换的条件**
转换前后的数据类型必须是兼容的
- 不能把对象类型转换成不相关类的对象
- **语法格式**
**强制类型转换的语法格式是在圆括号中给出想要转换的目标类型**,后面紧跟待转换的变量名<br />如`int x = (int) 1.5;`
- 如果试图将一个数值的类型强制转换为另一种类型,同时又超出了目标类型的表示范围,结果就会被截断成一个完全不同的值。
```java
double x = 9.997;
int ax = (int) x; //ax = 9
- 对于浮点型转整型的强制类型转换,浮点数的小数部分将被直接截断,仅保留整数部分。
- 如果想要得到浮点数最接近的整数,需要使用
Math.round()
方法对浮点数进行舍入计算double x = 9.997;
//ax = 10,这里仍然使用了强制类型转换是因为Math.round()返回long类型数据
int ax = (int) Math.round(x);
- 不能在
**boolean**
类型与任何数值类型之间进行强制类型转换,可能会出现错误
2.6.4 溢出与出错
- 表达式
**n%m**
当变量n(整数或浮点数类型)为负数时,无论m是多少都不会出错,但会有以下两种情况
- m等于2
表达式的结果为-1
- m等于除2以外的任何数
表达式结果为0
当n为正数时,m不能为0,否则会报错
当n为0时,m不能为0,但无论m为除0外的任何数,**0%m**
的结果都为0
- 对于计算出现溢出或出错,整型和浮点型产生的结果完全不同
- 溢出
- 整型
- 溢出
正溢出结果直接变为负数,负溢出结果直接变为正数
- **浮点型**
正溢出和分溢出的结果分别是Double.POSITIVE_INFINITY
、Double.NEGATIVE_INFINITY
,这两个常量的值是±Infinity
- 出错
- 整型
整型数计算出错时,编译器直接报错并退出
- **浮点型**
浮点型数计算出错时,返回结果Double.NaN
,其值为NaN(表示结果是非数值)
实例
public class MyClass {
public static void main(String[] args) {
double x = -1.7E308;
double y = -1.7E308;
System.out.println(x + y); //out: -Infinity
int z = 2147483647;
int w = 1;
System.out.println(z + w); //out: -2147483648
System.out.println(3.0 / 0); //out: Infinity
System.out.println(0.0 / 0); //out: NaN
System.out.println(Math.sqrt(-1)); //out: NaN
System.out.println(0 / 0); //编译器报错退出,无输出
}
}
不能直接使用
==
检测一个特定值是否等于Double.NaN
,检测的结果将永远是false
,如
0.0 / 0 == Double.NaN
的结果永远是false
,因为所有“非数值”的值都可以认为是不同的。
- 可以使用`Double.isNaN()`方法来判断一个值是否是“非数值”
###Math类API(java.lang包)
- Math类包含了用于执行基本数学运算的属性和方法,如初等指数、对数、平方根和三角函数。
- Math类的方法都被定义为 static 形式,通过Math类可以直接调用。
- 添加
**import static java.lang.Math.***
,静态导入Math类,即可直接使用Math类的方法和常量而不用添加前缀“Math”。
静态方法
**xxx Math.abs(xxx x)**
返回参数的绝对值,参数的类型xxx可以是6种基本数字类型
- 参数类型和返回值类型完全一致
double d = Math.abs(-0.05)
int i = Math.abs(-5)
float f = Math.abs(0.3f); //float f = Math.abs(0.3)无法通过编译,因为0.3是double类型
**double Math.ceil(float/double x)**
对x向上取整,返回大于等于x的整数,类型为double
System.out.println(Math.ceil(3.3)); //输出4.0
**double Math.floor(float/double x)**
对x向下取整,返回小于等于x的整数,类型为double
**int Math.round(float x)**
**long Math.round(double x)**
对浮点数进行四舍五入计算,得到最接近的整数
- 其内部算法相当于实现
Math.floor(x + 0.5)
Math.round(11.5); //out: 12 因为11.5+0.5再向下取整为12
Math.round(-11.5); //out: -11 因为-11.5+0.5再向下取整为-11
**xxx Math.max(xxx a, xxx b)**
、**xxx Math.min(xxx a, xxx b)**
返回两个参数中的较大值、较小值
**double Math.sqrt(double x)**
计算数值x的平方根。
**double Math.pow(double x, double a)**
计算x的a次幂(xa)
**double Math.random()**
返回带有正号的double值,从[0.0, 1.0)范围均匀分布伪随机地选择返回值。
- 用n乘以这个浮点数,就可以得到[0.0, n)范围内的一个随机浮点数。
**Math.PI**
、**Math.E**
这两个常量是Math类提供的两个π和e的近似值。
2.7流程控制
2.7.1 循环结构
Java中有三种主要的循环结构:
while
循环do…while
循环for
循环
for each循环
Java提供了一种增强型的for循环,称为for each循环,用于遍历实现了Iterable接口的类对象
- 语句格式:
for(variable : collection) statement
- 在数组一节将更详细的介绍该循环
在循环中,检测两个浮点数是否相等时需要格外小心,如下面的例子
for (double x = 0; x != 10; x += 0.1)
...
- 由于舍入的误差,浮点数可能永远达不到精确的最终值
- 上例就永远不会结束,因为0.1无法精确地用二进制表示,所以x将从9.9999…跳到10.0999…
2.7.2 break和continue
- 带标签的
**break**
带标签的break
用于跳出多重嵌套的循环语句。
jump_flag: //标签
while (true)
{
for(int i = 0; i< 100; i++)
{
if(i == 10)
break jump_flag; //跳出标签jump_flag紧跟的循环
}
}
- 标签必须放在希望跳出的最外层循环之前,并且必须紧跟着一个冒号
**:**
。 - 执行带标签的
break
后会跳转到带标签的语句块末尾。
**continue**
语句
continue
将中断正常的循环流程,将控制转移到最内层循环的首部。
while
循环内使用跳至“判断”部分
for
循环内使用则跳至“更新”部分,然后再进行“判断”。
- 带标签的
continue
将跳到与标签匹配的循环的首部。
- 事实上,可以将标签应用到任何语句,如下例子
label:
{
...
if(condition)
break label; //跳出语句块
}
//当condition满足,跳至此处
2.7.3 条件语句
- Java使用
if...else...
语句实现条件判断,具体细节同C++
2.7.4 switch case语句
switch case
语句用于判断一个变量与一系列值中某个值是否相等,每个值称为一个分支。语法格式
switch(variable){
case value1:
...
break; //可选
case value2:
...
break; //可选
... //可以有任意数量的case语句
default : //可选
...
}
switch case
语句有如下规则:**switch**
语句中的变量类型可以是
byte
、short
、int
、char
或String
(不可以是浮点数!)
case
语句中的值的数据类型必须与变量的数据类型相同,而且只能是常量。case
语句不必须要包含break
语句。- 当变量的值与某个
case
语句的值相等时,那么该case
语句之后的语句开始执行,如果该case
语句没有break
语句,那么继续执行后续case
语句以及default
语句,并且跳过判断过程,直到break
语句出现才跳出。 switch
语句可以包含一个default
分支,该分支一般是switch
语句的最后一个分支(可以在任何位置,但建议在最后一个)。default
在没有case
语句的值和变量值相等的时候执行。
3 Java常用内置类
3.1 字符串String
在Java中字符串属于对象,Java提供了
String
类来操作字符串。创建字符串
创建字符串有两种方式
直接将字符串常量赋值给字符串变量
String str = "Hello world!";
使用构造函数
String str = new String("Hello world!");
通过赋值创建的字符串存储在公共池中,而构造函数创建的字符串对象在堆上:
String s1 = "Runoob"; // String直接创建
String s2 = "Runoob"; // String直接创建
String s3 = s1; // 相同引用
String s4 = new String("Runoob"); // 构造函数创建
String s5 = new String("Runoob"); // 构造函数创建
每个用双引号
**""**
括起来的字符串,都是String类的一个实例。- 故在表示char类型数据时,必须使用单引号
''
- 故在表示char类型数据时,必须使用单引号
当将一个字符串与一个非字符串的值进行拼接时,后者会转换成字符串
- 任何一个Java对象都可以转换成字符串
String类没有提供修改字符串中某个字符的方法,即String类对象是不可变的。
检测字符串是否相等的方法(equals和==的区别)
- 使用
**string.equals(String s2)**
检测s1和s2是否完全相等。 - 使用
**equalsIgnoreCase()**
方法,检测两个字符串是否相等,但不区分大小写。 - 不能使用
**==**
运算符检测,其只能确定两个字符串是否存放在同一个位置上。 - 实际上所有引用类型都有这一特性,如数组等。
- 使用
空串
- 空串
**“”**
是一个String对象,有自己的串长度(0)和内容(空)。 - String变量还可以存放一个特殊的值
**null**
,表示没有任何对象与该变量关联。 - 空串
**“”**
不等于**null**
- 空串
字符串与基本数据类型的互相转化
- 基本数据类型转字符串
使用String类的静态方法valueOf()
/使用基本类型包装类的toString()
方法
- 字符串转基本数据类型
使用基本数据类型的包装类中的parseXxx()
方法,常用函数如下
Byte.parseByte(String s);
//以radix为基底,将s转换为byte。比如说Byte.parseByte("11", 16)会得到17
Byte.parseByte(String s, int radix);
Double.parseDouble(String s);
Double.parseFloat(String s);
Integer.parseInt(String s);
Long.parseLong(String s);
###String类API(java.lang包)
构造方法
**String()**
构造一个空字符串对象
**String(String original)**
构造一个值为original的字符串对象
**String(byte[] bytes)**
**String(char[] values)**
通过byte数组或char数组构造字符串对象
- byte数组的元素应该是哈希值,构造函数将把哈希值转换为对应的字符
**String(StringBuffer buffer)**
通过StringBuffer
对象构造字符串对象
静态方法
**String String.valueOf(xxx x)**
该方法可以将8种基本数据类型转换为String类
- 也可以传入Object对象obj,效果几乎等同于
obj.toString()
,但是当obj是null
时,采用该方法不会报错,但是直接使用obj.toString()
会抛出NullPointerException
异常。
可以参考String.valueOf(Object obj)
的源码
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}
动态方法
**char str.charAt(int index)**
返回指定索引处的字符。索引范围为从0
到length() - 1
。
- 注意,返回的是
char
类型
**int str.compare(String other)**
按照字典顺序,如果字符串位于other之前,则返回一个负数,相等返回0。
**boolean str.blank()**
、**boolean str.empty()**
判断字符串是否为空或者由空格组成,是返回true
**int str.indexOf(String str2, int fromIndex)**
返回与字符串str2
匹配的第一个子串的开始位置,从索引0
或fromIndex
开始匹配。
- 如果原始字符串
**str**
不存在**str2**
,返回**-1**
。 - 如果子串
**str**
是空串**""**
,返回**0**
**int str.length()**
返回str代码单元的个数。
- String类没有
size()
方法
**String str.replace(String oldString, String newString)**
返回一个新字符串,这个字符串用newString代替原始字符串str中所有的oldString。
**String str.substring(int beginIndex, int endIndex)**
返回一个新字符串,这个字符串包含原始字符串str中从beginIndex到endIndex-1的所有代码单元。
**String str.toLowerCase()**
、**String str.toUpperCase()**
返回一个新字符串,这个字符串将原始字符串中的所有大写字母改为小写字母,或将所有小写字母改为大写字母,原始字符串不变。
**String str.strip()**
返回一个新字符串,这个字符串将原始字符串str头部和尾部的空格删除。
**String str.join(String delimiter, String elements)**
返回一个新字符串,用给定的定界符连接所有元素
**String str.repeat(int count)**
返回一个新字符串,其内容是重复count次的str。
**char[] str.toCharArray()**
将字符串str
转换为字符数组并返回
**byte[] str.getBytes()**
使用平台的默认字符集将str编码为byte序列,并将结果存储到一个新的byte数组中。
- byte[]数组中存放的是字符串相应位置字母的哈希值,如字符串中的字母a对应byte[]数组中的97
3.2 StringBuilder和StringBuffer
当需要对字符串进行修改,或者需要用许多字符串来拼接成一个字符串时,需要使用
StringBuilder
类或StringBuffer
类**String**
、**StringBuilder**
和**StringBuffer**
的异同String
长度、内容不可变;StringBuffer
和StringBuilder
长度、内容可变**StringBuffer**
线程安全;**StringBuilder**
线程不安全**StringBuffer**
速度慢;**StringBuilder**
速度快StringBuilder
和StringBuilder
提供的方法相同,并且基本都实现了String
的方法
- 应该在几乎所有的情况下选择使用
**StringBuilder**
字符串构建器
**StringBuilder**
- 如果需要用许多小段的字符串来构建一个字符串,那么需要用到字符串构建器。因为字符串多次使用
+
拼接效率低 String
类是不可改变的,如果需要对字符串做很多修改,那么应该选择使用StringBuilder
类,因为StringBuilder
能够被多次原地修改,不产生新的未使用对象。- 实例 ```java //构建一个空的字符串构建器 StringBuilder builder = new StringBuilder();
- 如果需要用许多小段的字符串来构建一个字符串,那么需要用到字符串构建器。因为字符串多次使用
//当每次需要添加一部分内容时,就调用append()方法 builder.append(‘a’); builder.append(‘b’); builder.append(‘c’);
//当字符串构建完成时,调用toString()方法即可得到一个String对象 System.out.println(builder.toString()); //out: abc builder.reverse(); System.out.println(builder.toString()); //out: cba
<a name="28844f93f688cf6e63bb820620697bbb"></a>
### ###StringBuffer类API###
**构造方法**
- `**StringBuilder(CharSequence seq)**`
返回一个StringBuilder对象,其值为seq
- **实现了CharSequence接口的类都可以传入,即**`**String**`**、**`**StringBuilder**`**或**`**StringBuffer**`
**动态方法**
- 在使用`StringBuilder`类时,**每次都会对**`**StringBuilder**`**实例本身进行原地操作**
- `StringBuilder sb.append(String s)`
- `**StringBuilder sb.append(char[] s)**`
- `StringBuilder sb.append(char c)`
- `StringBuilder sb.append(int s)`
...<br />将指定的参数原地追加到此字符序列。
- `StringBuilder sb.reverse()`
原地反转字符序列
- `**StringBuilder sb.delete(int start, int end)**`
原地移除字符序列位置在[start, end)的字符。
- `StringBuilder sb.insert(int pos, XXX x)`
将参数`x`(可以是`String`、`char`、`int`等多种类型)插入此序列的`pos`位置。
- `StringBuilder sb.replace(int start, int end, XXX x)`
使用给定参数替换此序列位置在[start, end)的字符。
<a name="8f21d30658d54f8dbde668f44c3e1a4b"></a>
## 3.3 控制台输入与输出
<a name="a2ccf987be2b9106ca0d1e138f9839f0"></a>
### 3.3.1 控制台输入
- **读取“标准输入流”**`**System.in**`
```java
//创建与System.in关联的Scanner对象。
Scanner in = new Scanner(System.in);
- 构造完成后就可以使用
**Scanner**
类的各种方法读取输入了。
**next()**
下面的实例是一个最简单的数据输入,并通过Scanner类的next()
方法获取输入的字符串,在读取前我们一般需要使用hasNext()
判断是否还有输入的数据
public class MyClass {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
//从键盘接收数据
//next方式接收字符串
System.out.println("next方式接收:");
//判断是否还有输入
if (scan.hasNext()) {
String str1 = scan.next();
System.out.println("输入的数据为:" + str1);
}
scan.close();
}
}
- 上述代码的输出为(
next()
方法以空格作为结束符)next方式接收:
cui yi chen
输入的数据为:cui
**nextDouble()**
、**nextInt()**
…
除了使用next()
接收字符串,还可以使用nextXxx()
接收数字,如nextDouble()
、nextInt()
等。
以下实例我们可以输入多个数字,并求其总和与平均数,每输入一个数字用回车确认,通过输入非数字来结束输入并输出执行结果:
public class MyClass {
public static void main(String[] args) {
System.out.println("请输入数字:");
Scanner scan = new Scanner(System.in);
double sum = 0;
int m = 0;
while (scan.hasNextDouble()) {
double x = scan.nextDouble();
m = m + 1;
sum = sum + x;
}
System.out.println(m + "个数的和为" + sum);
System.out.println(m + "个数的平均值是" + (sum / m));
scan.close();
}
}
- 上述实例的输出为
请输入数字:
1 2 3
#
3个数的和为6.0
3个数的平均值是2.0
**nextLine()**
上面的方法都是以空格为结束输入的标志,那么如果想要获得一整行可以带有空格的输入,则需要使用nextLine()
方法,该方法以换行符为结束标志
public class MyClass {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
System.out.println("nextLine方式接收:");
if (scan.hasNextLine()) {
String str1 = scan.nextLine();
System.out.println("输入的数据为:" + str1);
}
scan.close();
}
}
- 上述代码输出为
nextLine方式接收:
cui yi chen
输入的数据为:cui yi chen
nextLine()与其他方法的区别
**nextLine()**
与其他方法的区别
其他方法(如**next()**
、**nextInt()**
等):
- 一定要读取到有效字符后才可以结束输入(只输入空白符无法结束)。
- 对输入有效字符之前遇到的空白,会自动将其去掉。
- 只有输入有效字符后才将其后面输入的空白作为分隔符或者结束符。
next()
不能得到带有空格的字符串。
**nextLine()**
:
- 以换行符为结束符,也就是说
**nextLine()**
方法返回的是输入回车之前的所有字符。 - 在读取到有效字符前可以结束输入,即如果直接输入换行符,将会直接结束。
- 可以获得空白,即空白也作为有效字符。
- 所有方法都不接收其本次输入对应的结束符,即
nextLine()
不接收换行符,其他方法不接收空白符。
需要注意的是,**nextLine()**
会把其结束符从输入缓存中剔除,而其他方法并不会把其结束符从输入缓存中剔除
Scanner scan = new Scanner(System.in);
System.out.print("输入:");
System.out.println("next()接收到的:" + scan.next());
System.out.println("nextLine()接收到的:" + scan.nextLine());
System.out.print("输入:");
System.out.println("nextLine()接收到的:" + scan.nextLine());
scan.close();
- 上述代码的输出为
输入:cui yi chen
next()接收到的:cui
nextLine()接收到的: yi chen
输入:yi chen
nextLine()接收到的:yi chen
**nextLine()**
与**next()**
、**nextInt()**
等混合使用时需要特别注意
由于除了nextLine()
外,其他方法不会把其结束符从输入缓存中剔除,因此**nextLine()**
可能会接收到**next()**
等方法的结束符,如空格,换行符等,如果是换行符,那么**nextLine()**
将直接结束接收,如下例子
public class MyClass {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int i = scan.nextInt();
String line = scan.nextLine();
System.out.println("nextInt()接收到的:" + i);
System.out.println("nextLine()接收到的:" + line);
scan.close();
}
}
- 上述代码输出(在输入5后直接回车)
输入:
5
nextInt()接收到的:5
nextLine()接收到的:
###Scanner类API(java.util包)
- 当使用的类不是定义在基本java.lang包中时,一定要使用
import
指令导入相应的包。 - Scanner类定义在java.util包中,故程序的最前面应添加
**import java.util.***
构造函数
**Scanner(InputStream source)**
构造一个从输入流中读取数据的Scanner对象
- 我们常需要从键盘读取数据,那么就可以将标准输入流
System.in
作为参数传入
**Scanner(Path p, String encoding)**
构造一个使用给定字符编码从给定路径的文件中读取数据的Scanner对象。
- 第一个参数常使用静态方法
**Path.of(String filePath)**
(java.nio.file.*),将String
转换为Path
动态方法
**String scnr.nextLine()**
读取输入的下一行内容(读取的内容包括一行内的空格,以换行符作为结束符)
**String scnr.next()**
读取输入的下一个字符串(以空白符作为结束符)
**int scnr.nextInt()、**``**double scnr.nextDouble()**
读取并转换下一个表示整数或浮点数的内容(以空白符作为结束符)
**boolean scnr.hasNext() / scnr.hasNextInt() / scnr.hasNextDouble()**
检测是否还有下一个字符串/整数/浮点数。
3.3.2 控制台输出
- 打印“标准输出流”
**System.out**
```java //可以使用以下语句将内容x打印到“标准输出流(即控制台窗口)” System.out.print(x); System.out.println(x); System.out.printf(x);
//Java沿用了C语言函数库中的printf方法,可以进行格式化的输出 //%s转换符可以将任意对象格式化为字符串。 System.out.printf(“name: %s score: %8.2f”, name, score);
- **对于日期与时间的格式化选项**,应使用java.time包中的方法。
- **创建指定格式的字符串**
可以使用`**String.format()**`方法创建一个指定格式的字符串,而不打印输出
```java
String message = String.format(“name: %s score:%8.2f”, name, score);
3.5 大数
java.math包中的两个类
**BigInteger**
(大整数)和**BigDecimal**
(大实数)可以处理包含任意长度数字序列的数值。创建大数实例
A. 使用上述两个类的静态方法valueOf(num)
可以将普通的数值转换为大数。如:
BigInteger a = BigInteger.valueOf(1000);
B. 对于过大/精密而无法使用基本类型表示的数,可以使用带字符串参数的构造器。如:
BigInteger b = new BigInteger("12345678910111213141516");
- 大数的计算
不能使用人们熟悉的算术运算符计算大数或是比较大数,而应该使用大数类提供的计算方法。如big1.add(big2)
###BigInteger类API(java.math包)
构造函数
BigInteger BigInteger(String num)
构造一个值为num的大整数
静态方法
BigInteger BigInteger.valueOf(int num)
返回一个值为num的大整数
动态方法
BigInteger bi.add(BigInteger bi2)
BigInteger bi.subtract(BigInteger bi2)
BigInteger bi.multiply(BigInteger bi2)
BigInteger bi.divide(BigInteger bi2)
BigInteger bi.mod(BigInteger bi2)
返回大整数bi和另一个大整数bi2的和、差、积、商以及余数
BigInteger bi.sqrt()
得到大整数bi的平方根
int bi.compareTo(BigInteger bi2)
如果大整数bi与另一个大整数bi2相等,返回0;如果bi小于bi2,返回负数;否则,返回正数。
###BigDecimal类API(java.math包)
构造函数
BigDecimal(String num)
构造一个值为num的大实数
静态方法
BigDecimal.valueOf(int/float/double num)
返回一个值为num的大实数
动态方法
BigDecimal bd.add(BigDecimal bd2)
BigDecimal bd.subtract(BigDecimal bd2)
BigDecimal bd.multiply(BigDecimal bd2)
返回大实数bd与bd2的和、差、积。
BigDecimal bd.divide(BigDecimal bd2)
BigDecimal bd.divide(BigDecimal bd2, RoundingMode mode)
返回大实数bd与bd2的商。
- 如果商是个无限循环的小数,第一个divide方法会抛出一个异常。
- 要得到一个舍入的结果,就要使用第二个方法,即向函数传入指定的舍入方法。(
RoundingMode.HALF_UP
是四舍五入的方式)
int bd.compareTo(BigDecimal bd2)
如果大实数bd与另一个大实数bd2相等,返回0;如果bd小于bd2,返回负数;否则,返回正数。
3.6 数组
什么是数组?
- 数组是存储相同类型值的序列
- 数据类型后紧跟[]表示数组类型(如
int[]、String[]
等) - 数组类型不属于基本类型,和其他类一样,继承自Object类
数组声明
int[] a;
- 这条语句只声明了变量a,并没有将a初始化为一个真正的数组。
- C语言风格的声明也是可行的,即int a[],但不常用。
数组初始化 ```java //方法一 int[] a = new int[100]; //声明并初始化了一个可以存储100个整数的数组
//方法二 int[] b = {1,2,3,4,5}; //这种方式不需要使用new,也不用指定长度
- **数组长度不要求是常量**,new int[n]会根据变量n的大小创建数组。
- 一旦初始化了数组,就**不能再改变它的长度**。
- 程序运行中**如果需要扩展数组的大小**,则**应使用数组列表(ArrayList)**这一数据结构。
- **方法二仅适用于变量声明时初始化**,声明变量后将无法使用方法二初始化,但是可以使用匿名数组的方式
```java
int[] i = new int[3];
int[] j;
int[] k = {1, 2, 3}; //编译通过
i = {1, 2, 3}; //编译不通过
j = {1, 2, 3}; //编译不通过
i = new int[]{1, 2, 3}; //编译通过
- 数组默认值
初始化数组但没有为其赋初始值时,数组将被赋予默认值,不同的数组类型默认值不同
- 数组变量无论是局部变量、类变量还是实例变量,如果没有被初始化,都会被赋予默认值
这里与普通的变量不同(普通变量是局部变量时不会被赋予默认值)
- 数字数组:默认值为0
- boolean数组:默认值为false
- 对象数组:默认值为一个特殊值null,表示这些元素还未存放任何对象
- 数组长度
数组长度使用数组的属性**length**
来获取
int[] a = new int[10];
System.out.println(a.length); //out: 10
String
的长度使用**str.length()**
获取
- 匿名数组
匿名数组是一个没有名字的数组,不需要为其指定变量名(也可以指定),一般适用于仅使用一次的情况
System.out.println(Arrays.toString(new String[]{"abc", "de"}))
for each循环
- for each循环可以依次处理数组(或其他的元素集合)中的每个元素,而不必考虑指定下标值。
for each循环的语句格式:
for(variable : collection) statement
- 该语句定义一个变量
**_variable_**
用于暂存集合中的每一个元素,并且必须指定其元素类型。 **_collection_**
这一集合表达式必须是一个数组或者是实现了Iterable接口的类对象- eg:
int[] ar = {1, 2, 3, 4};
//打印数组的每个元素
for (int element : ar)
System.out.print(element);
- 该语句定义一个变量
如果不希望遍历整个集合,或者在循环内部需要使用下标值时,传统的for循环更为适用
数组的拷贝
- 使用
**=**
将一个数组变量直接拷贝到另一个数组变量时,两个变量将引用同一个数组 - 如果希望将一个数组的所有值拷贝到一个新的数组中去,则要使用Arrays类的
**copyOf()**
方法。
- 使用
多维数组/不规则数组
- 多维数组初始化时必须给出第一维的长度,如
int[][][] ar = new int[3][][];
- Java支持不规则数组,即数组除了第一维外,其他维的长度可以不统一,如下例子
int[][] odds = new int[max + 1][];
//分别为每一维分配空间
for (int n = 0; n <= max; n++)
odds[n] = new int[n + 1];
- 多维数组初始化时必须给出第一维的长度,如
输出数组内容/将数组转为字符串
A. 循环数组,逐元素输出
最简单、常用的方式,可以使用for循环或for each循环遍历数组
int[] a = {1, 2, 3};
for (int i = 0; i < a.length; i++)
System.out.print(a[i]);
//输出123
B. 将数组转为字符串,作为整体输出
数组不能直接输出,也不能直接使用.toString()方法将其转换为字符串
- Java里,所有的类,不管是java库里面的类,或者是你自己创建的类,全部是从Object这个类继承的。
Object类一个方法就是
toString()
,那么所有的类创建的时候,都有一个toString()
的方法。
- Java输出用的函数
print()/println()
,是不接受对象直接输出的,只接受字符、字符串或者数字之类的输出。当
**print()**
检测到输出的是一个非String对象而不是字符串、字符或数字时,那么它会去调用这个对象类里面的toString()
方法,输出结果为“类型@哈希值”,如[I@7c30a502。
将数组转换为字符串,转换结果有两种形式
- 形式1:“[1, 2, 3]”
使用Arrays.toString()
方法,转换的结果是带格式的
- 形式2:“123”
有两类方法进行转换
a) 将数组作为构造String的数据或调用String类的**valueOf()**
方法
只适用于数组类型为char[]
或byte[]
的数组
char[] data = {'a', 'b', 'c'};
//将数组作为构造String的数据
String str = new String(data);
调用String.valueOf()方法
String str2 = String.valueOf(data)
b) 循环遍历数组配合字符串构造器**StringBuilder**
适用于所有基本数组类型
int[] a = {1, 2, 3};
StringBuilder builder = new StringBuilder();
for (int i = 0; i < a.length; i++)
builder.append(String.valueOf(a[i]));
String str = builder.toString();
- 杂项
- 长度为0的数组与null并不相同,即new int[0]≠null
- 数组.length将返回数组的长度。
###Arrays类API(java.util包)
**Arrays**
类能方便地操作数组,它提供的所有方法都是静态的。
静态方法
T[]
可以指代基本数据类型和引用数据类型的数组String Arrays.toString(T[] a)
返回包含a中元素的一个字符串,这些元素用中括号包围,并用逗号分隔。
- 将a转换为字符串时,不能直接使用Object类的toString()方法
**void Arrays.sort(T[] a)**
使用优化的快速排序算法对数组进行原地排序。
**int Arrays.binarySearch(T[] a, T v)**
**int Arrays.binarySearch(T[] a, int start, int end, T v)**
使用二分查找算法在有序数组a中查找指定值v。
- 如果找到v,返回相应的下标,否则返回一个负数值r(r = -(应插入位置) - 1)。
**void Arrays.fill(T[] a, T v)**
将数组的所有元素原地设置为v
**boolean Arrays.equals(T[] a, T[] b)**
如果两个数组大小相同,并且下标相同的元素都对应相等,返回true。
- 和String的比较一样,数组类型变量直接使用==进行比较,实际上是判断两个变量是否引用同一地址。
**List<T> Arrays.asList(T[] a)**
将数组a
转为List
**T[] Arrays.copyOf(T[] original, int newLength)**
返回一个新的数组,其内的值复制original数组中的值,如果新数组的长度超过原数组的长度,则超出部分的值为默认值
4 类与对象
1 类概述
- 类(class)
类是构造对象的模板。
- 类构造(construct)对象的过程称为创建类的实例(instance)。
- 对象中的数据称为实例字段(instance field),操作数据的过程称为方法(method)。
- 在Java中,所有的方法都必须在类的内部定义。(与C++不同)
- 作为一个类的实例,特定对象都有一组特定的实例字段值,这些值的集合就是这个对象的当前状态,只要在对象上调用一个方法,它的状态就有可能发生改变。
- 封装(encapsulation)
封装就是将数据和行为组合在一个包中,并对对象的使用者隐藏具体的实现方式。
- 实现封装的关键在于,绝对不能让类中的方法直接访问其他类的实例字段。程序只能通过对象的方法与对象数据进行交互。
- 封装是提高重用性和可靠性的关键,即其他对象不会知道也不用关心这个类的变化。
- 继承(inheritance)
通过扩展一个类来建立另一个类的过程称为继承。这个扩展的新类具有被扩展的类的全部属性和方法
- 所有类,不管是Java库里面的类,或者是你自己创建的类,都继承自Object类。
- 识别类
面向对象程序设计时没有所谓的“顶部”(过程式程序中的main函数),而是从“识别类”开始,然后再为各个类添加方法。
- 识别类的一个简单经验是在分析问题的过程中寻找名词,类对应着名词,而类的方法对应着动词。
- 并不是所有类都表现出面向对象的所有典型特征
比如预定义类Math,我们可以直接使用Math类的方法。
- Math类只封装了功能,它不需要也不必隐藏数据。
- 由于没有数据,因此也不必考虑创建对象和初始化它们的实例字段,因为根本没有实例字段。这也是Math类只包含静态方法的原因。
- 类与类之间有三大关系
- 依赖(uses-a)
- 如果一个类的方法使用或操纵另一个类的对象,我们就说一个类依赖于另一个类。
- 应该尽可能地将相互依赖的类减至最少,也就是减少类之间的耦合。
- 聚合(has-a)
- 包容关系意味着类A的对象包含类B的对象。
- 继承(is-a)
- 如果类A扩展类B,类A不但包含从类B继承的方法,还会有一些额外的功能。
可以采用UML(Unified Modeling Language,统一建模语言)绘制类图,用来描述类之间的关系。
1 Object类
- Object类是Java所有类的祖先,每个类都直接或间接继承自Object,包括数组和自定义类。
- 所有类都继承自Object类的目的主要是使用Object类的方法,这些方法对所有类具有普适性。
当然,很多类需要根据实际情况重写继承自Object类的方法。
Java只支持单继承,如果类未声明指定的父类,默认为Object。
Object的变量可以保存任何类的对象的引用
但是其运行期类型仍是其保存的对象引用的真实类型
Object str = "Good";
Object nowDate = LocalDate.now();
//输出java.time.LocalDate
System.out.println(nowDate.getClass().getName());
当
**return**
类型声明为Object时,可以返回任意类型的对象但是接收返回的变量必须是Object类型,除非对返回值进行强制类型转换
public class MyClass {
public static void main(String[] args) throws IOException {
String str = (String) returnObject(); //str = "Good"
String astr = returnObject(); //编译错误
}
public static Object returnObject() {
return "Good";
}
}
2 (待)###Object类API(java.lang包)
动态方法
**final Class obj.getClass()**
返回一个对象的运行期类型。
常使用**String class.getName()**
方法获取类型名
Object str = "sdasds";
//输出运行期类型class java.lang.String而不是class java.lang.Object
System.out.println(str.getClass());
//输出java.lang.String
System.out.println(str.getClass().getName());
**(待)boolean obj.equals(Object obj1)**
判断两个obj的地址是否相同,或者说是否引用了同一个对象
- 这里Object类原始的
equals()
方法并不是从逻辑上判断对象是否相等的,而是通过地址。
equals()
源码:
public boolean equals(Object obj){
return(this==obj)
}
从源码可以看到,原始的**equals()**
方法效果等同于**==**
- 重写
**equals()**
由于obj.equals()
等价于==
,因此,几乎所有类都需要重写**equals()**
方法,实现判断两个对象是否逻辑相等(内容相等)的功能
如果要重写**equals()**
方法,就必须重写**hashCode()**
方法,具体见https://blog.csdn.net/We_chuan/article/details/96426273
- 如String类的`equals()`方法源码
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String aString = (String)anObject;
if (coder() == aString.coder()) {
return isLatin1() ? StringLatin1.equals(value, aString.value)
: StringUTF16.equals(value, aString.value);
}
}
return false;
}
**(待)int obj.hashCode()**
返回对象的hash码
- hash码表示对象在hash表上位置(注意:该位置可以挂载多个不同的对象),不是内存地址
因此,hash码不能够代表内存地址,如下例,两个对象hash码相等,但引用的不是一个对象
LocalDate ld = LocalDate.of(1900, 1, 1);
LocalDate ld2 = LocalDate.of(1900, 1, 1);
System.out.println(ld.hashCode() == ld2.hashCode()); //输出true
System.out.println(ld == ld2); //输出false
- 同一个对象与hashCode
- 内容相等的对象与hashCode
对于各种预定义类重写过的equals()
方法来说
两个内容相同的对象(两个相等的对象)equals()
方法一定为true
(但hashCode不一定相等)
结论:
对象相等 ⇏ hashCode相等
hashCode相等⇏ 对象相等
- 关于hashCode更详细的内容,见https://www.cnblogs.com/zhchoutai/p/8676351.html
**String obj.toString()**
返回对象信息
源码
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
Object类中的
toString()
方法只是返回类的类名和hashCode
如数组的toString()
方法是继承自Object类的
int a = {1, 2, 3};
System.out.println(a.hashCode()); //输出[I@7f63425a
- 可以重写
toString()
方法(部分需要打印的类重写了**toString()**
方法),来实现自己所需要的打印信息功能。 - 如LocalDate类重写了
toString()
方法,方便打印该类所存时间public final class LocalDate
implements Temporal, TemporalAdjuster, ChronoLocalDate, Serializable {
...
@Override
public String toString() {
int yearValue = year;
int monthValue = month;
int dayValue = day;
int absYear = Math.abs(yearValue);
StringBuilder buf = new StringBuilder(10);
if (absYear < 1000) {
if (yearValue < 0) {
buf.append(yearValue - 10000).deleteCharAt(1);
} else {
buf.append(yearValue + 10000).deleteCharAt(0);
}
} else {
if (yearValue > 9999) {
buf.append('+');
}
buf.append(yearValue);
}
return buf.append(monthValue < 10 ? "-0" : "-")
.append(monthValue)
.append(dayValue < 10 ? "-0" : "-")
.append(dayValue)
.toString();
}
...
}
**protected Object obj.clone()**
- 创建一个新对象,然后将当前对象的非静态字段复制到该对象
- 如果字段类型是值类型(基本类型)的,那么对该字段进行复制;
- 如果字段是引用类型的,则只复制该字段的引用而不复制引用指向的对象。
此时新对象里面的引用类型字段相当于是原始对象里面引用类型字段的一个副本,原始对象与新对象里面的引用字段指向的是同一个对象
上述描述属于浅克隆方式
源码
protected native Object clone() throws CloneNotSupportedException;
使用
**clone()**
如果想要在自己的类外调用自己类实例的**clone()**
方法,需要实现**Cloneable**
接口,同时需要重写**clone()**
方法(需要抛出**CloneNotSupportedException**
异常)。
- **派生类必须实现**`**Cloneable**`**接口的原因:**
Cloneable
接口是空的,它仅仅是一个标志,派生类实现了这个接口后,在类外使用clone方法就不会抛出**CloneNotSupportedException**
异常了。
- **必须要重写**`**clone()**`**方法的原因:**
Object类中的**clone(**
)方法是**protected**
权限,只能在本包的子类外部或者在其它包的子类内部访问,这里重写一下,虽然还是**protected**
的,但是可以在本包的子类外部访问了(当然也可以重写为**public**
)
- 根据实际情况的不同,重写
**clone()**
方法有两种思路
**
实现Cloneable()
接口,然后在重写的clone()
方法内直接调用obj.clone()
方法
//此处实现Cloneable接口
class MyClass implements Cloneable{
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone(); //调用obj.clone()
}
}
B. 深克隆——clone()方式
如果对象中有引用数据类型,就必须使用深度克隆(防止类的封装性被破坏)。
具体来说,就是在**clone()**
方法内部把该对象实例变量中引用的其他对象也要clone一份,这就要求被实例变量引用的对象也必须要实现**Cloneable**
接口并且重写**clone**
方法。
public class MyClass implements Cloneable {
private B b;
private C c;
//深克隆
protected Object clone() throws CloneNotSupportedException {
MyClass myclass = null;
try {
myclass = (MyClass) super.clone(); //浅克隆,clone对象本身是基础
myclass.b = (B) this.b.clone(); //手动clone成员对象引用b,并赋值给新MyClass对象
myclass.c = (C) this.c.clone(); //手动clone成员对象引用c,并赋值给新MyClass对象
} catch (CloneNotSupportedException e) {
}
return myclass;
}
}
class B {
//浅克隆
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
class C {
//浅克隆
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
- 这种方法的缺点就是当一个类里面有很多引用类型时,需要手动调用很多`clone()`,而且如果引用类型内部还有引用类型时,那么代码将会很恶心,量也很大。。。
- 所以**这种方式一般用于引用类型变量较少的时候**。
对于很多引用类型,可以使用序列化对象的方式进行深拷贝。
C. 深克隆——序列化方式
这种方式其实就是将对象转成二进制流,然后再把二进制流反序列成一个Java对象,这时候反序列化生成的对象是一个全新的对象,里面的信息与原对象一样,但是所有内容都是一份全新的。
这种方式需要注意的地方主要是除了实现都需要实现Cloneable
接口外还需要实现Serializable
接口,以便进行序列化操作。
2 对象与对象变量
- 对象的定义
- 对象是类的一个实例。
- 所有的Java对象都存储在堆中。
- 对象的三个特征
- 对象的行为(behaivor)
用可调用的行为来定义。
- 对象的**_状态(__state__)_**
状态就是对象描述当前状况的信息
- 对象的**_标识(__identity__)_**
对象的状态并不能完全描述一个对象,每个对象都有一个唯一的标识。
- 构造对象
要想使用对象,首先必须构造对象,然后对对象应用方法。
要使用构造器(或称构造函数)构造新实例。
- 构造器的名字应该与类名相同
- 要想构造一个对象,需要在构造器前面加上new操作符,如
new Date()
。
- 对象变量
通常需要多次使用我们构造的对象,因此需要将对象存放在一个变量中
//构造了一个Date对象,并存入today变量,其被初始化为当前日期和时间
Date today = new Date();
- 对象变量
today
引用了新构造的Date
对象。 - 在对象与对象变量之间存在着一个重要的区别:
变量在引用对象前(被初始化前)不是一个对象,其值为null
,表示没有引用任何对象,此时还不能在这个变量上使用任何对象的方法
- 对象变量被初始化后,并没有实际包含一个对象,它只是引用一个对象。
表达式new Date()
构造了一个Date类型的对象,它的值是对新创建对象的一个引用,这个引用存储在变量today
中。
可以把Java中的对象变量看作类似于C++的对象指针。
Date* birthday = new** **Date(); _//C++_
- 对象的复制 ```java Date date = new Date(); Date copy1 = date; Date copy2 = (Date) date.clone();
System.out.println(date == copy1); //输出true System.out.println(date == copy2); //输出false
- 上述语句中,`date`和`copy1`引用的是**同一个对象**。
因此,如果通过某个变量修改对象的实例字段,另一个变量所引用对象的实例字段将有完全相同的改变。
- 因此,在Java中,必须使用Object类的`**clone()**`**方法**获得**对象的完整副本**
- 注意`clone()`返回Object类型,需要强制转换类型。
- 可以使用双等号`==`来比较两个对象变量所引用的地址是否相同,进而判断是否引用了同一个对象。
<a name="t0qg3"></a>
### 1 关于null引用
- 我们已经知道,一个对象变量**包含一个对象的引用**,或者**包含一个特殊值**`**null**`
- `**null**`**表示对象变量没有引用任何对象**
- 基本数据类型不可能是`null`
- `null`可以被强制转换为任何Java类型
- `null`引用是一种处理特殊情况的便捷机制,然而使用`null`值必须非常小心
**如果对值是**`**null**`**的对象变量应用一个方法,会产生一个**`**NullPointerException**`**异常**。正常情况下,程序并不捕获这些异常,出现此异常程序就会终止。
- 定义一个类时,**需要清楚地知道哪些字段可能为**`**null**`
```java
public class Employee {
private String name;
private LocalDate hireDay;
private double salary;
public Employee(String n, double s, intyear, int month, int day) {
name= n;
salary= s;
hireDay= LocalDate.of(year, month, day);
}
}
- 在上述代码中,我们不希望
name
和hireDay
字段为null
。 hireDay
字段肯定是非null
的,因为它初始化为一个新的LocalDate
对象,salary
字段也不用担心,因为它是基本类型。name
可能是null
类型
当类中某些字段可能是
**null**
,有两种方案可以应对“宽容型”方案:把
null
参数转换为一个适当地非null
值public class Employee {
private String name;
public Employee(String n) {
if (n == null)
name ="unknow";
else
name= n;
}
}
**Objects**
类提供了一个替代上述代码的便利方法:name = Objects.requireNonNullElse(n,"unknown");
“严格型”方案:拒绝
null
参数 ```java
public class Employee { private String name; public Employee(String n) { Objects.requireNonNull(n, “The name cannot be null!”); //当n是null时报错 name = n; } }
- 当参数`n`的值是`null`时,就会产生`NullPointerException`异常**,**且异常报告会提供这个问题的描述和指出问题发生的位置**。**
- 否则,`NullPointerException`异常可能会在其他地方出现,而很难追踪到真正导致问题的这个构造器参数。
<a name="kxbkX"></a>
### 2 ###Objects类API(java.util包)###
- **Objects类用于“优雅地”处理**`**null**`
**静态方法**
- `static <T> void **requireNonNull**(T obj)`
- `static <T> void **requireNonNull**(T obj, String message)`
如果obj为`null`,这些方法会抛出一个`NullPointerException`异常,同时不提供或提供一则给定的message。
- `static <T> T **requireNonNullElse**(T obj, T defaultObj)`
如果obj不为`null`则返回obj,否则返回默认的obj。
<a name="DctBC"></a>
## 3 类详解
- **最简单的类定义形式:**
```java
class ClassName
{
//实例字段和类字段
field1
field2
……
//构造器
constructor1
constructor2
……
//方法
method1
method2
……
}
- 要想构建一个完整的程序,一般会结合使用多个类。
1 public class和class
- 如果一个源文件包含多个类,且其中某个类带有public修饰符,则源文件名必须与public类的名字相匹配。
- 在一个源文件中,只能有一个public class,但可以有任意数目的class(非公共类)。
- 当编译代码时,编译器会为每个类创建类文件,如a.class和b.class。
- 许多程序员习惯将每一个类存放在一个单独的源文件中,如将a类存放在文件a.java中,将b类存放在b.java中。
2 类名的命名规范
- Java类名标准的命名规范是驼峰命名法
类名是以大写字母开头的名词,如果名字由多个单词组成,每个单词的第一个字母都应该大写
2 main()方法
**main()**
方法是Java应用程序的入口方法
也就是说,程序在运行的时候,第一个执行的方法就是main()
方法。
main()
方法必须是**public static void**
类型的,且必须接收一个字符串数组的参数。public class MyClass {
public static void main(String[] args) throws IOException {
}
}
main
方法一定是静态的,因为main方法不对任何对象进行操作,实际上,在启动程序时还没有任何对象。- 静态的main方法将执行并构造程序所需要的对象。
main方法所在类允许不加public关键字约束,但eclipse默认到public的类中去找main函数(可修改),为了方便还是建议把main函数放在public类中。
每一个类都可以有一个main方法,这是常用于对类进行独立单元测试的一个技巧。
- 只有直接执行某个类时,其方法才会执行,否则永远不会执行(除非直接调用它)。
将程序中包含main方法的类名提供给字节码解释器,以启动程序。
3 方法重载
- 重载的定义
重载是指一个类可以有多个相同名字的方法,但它们的参数必须不同(数量或类型)。
- 方法的签名
- 完整地描述一个方法,需要指定方法名以及参数类型,这称为方法的签名。
- 虽然类可以通过重载拥有多个相同名字的方法,但每个方法的签名必须不同
- 返回类型不是方法签名的一部分,
即不能有两个名字相同,参数相同却有不同返回类型的方法。
4 更改器方法和访问器方法
类的所有方法,根据是否改变对象的状态可以分为更改器方法和访问器方法。
更改器方法
当对象调用更改器方法后,对象的状态会发生改变。
- 访问器方法
只访问对象而不修改对象的方法称为访问器方法,如LocalDate.getYear()
和String_._toUpperCase()
等
3 访问控制修饰符
Java提供了两类修饰符
- 访问控制修饰符
- 非访问控制修饰符
两类修饰符并不互斥,但访问控制修饰符之间互斥、非访问修饰控制符局部互斥
本节主要介绍访问控制修饰符
修饰符用来修饰类(class)、方法(method)和实例字段/类字段(field),通常放在语句的最前端。
针对访问级别的修饰符可以对类(class)、方法(method)和实例字段/类字段(field)进行修饰,用来保护对类、字段、和方法的访问。
Java 支持4种不同的访问权限:
default
public
private
protected
3.1 默认访问修饰符——default
default即不使用任何修饰词,不指定修饰词的部分可以被同一个包中的所有方法访问。
- 访问范围:在同一包内可见
- 修饰对象:类、方法、字段
- 应用
除了与源文件同名的类,其他类通常不指定修饰符
3.2 公有访问修饰符——public
public
标记的部分意味着任何类的任何方法都可以使用。
- 访问范围:对所有类可见。
- 修饰对象:类、方法、字段
- 应用
public
通常被用来修饰类中的方法或与源文件同名的类,某些辅助方法不应该成为公共接口的一部分,应该将其设置为私有方法。
- 由于类的继承性,类所有的公有方法和变量都能被其子类继承。
main()
方法必须是public
修饰
3.3 私有访问修饰符——private
private
是最严格的访问级别,被private
标记的部分确保只能由定义它们的类使用,而其他类不能使用。
- 访问范围:在同一类内可见
- 修饰对象:方法、字段 (注意:不能修饰类(内部类除外))
- 应用
类中的实例字段通常需要显式地标记为private
,不然会破坏封装性。
3.4 受保护的访问修饰符——protected
被protected
修饰的成员对于本包的所有类和其子类内部可见,子类和同一个包的类可以访问protected
修饰符声明的方法和字段,能防止不相关的类使用这些方法和字段。
- 访问范围:对同一包内的类和所有子类可见。
- 修饰对象:方法、字段 (注意:不能修饰类(内部类除外),接口及接口的字段和方法不能声明为protected)
当子类与基类不在同一包中,子类对基类
protected
成员的并不是完全可见的:- 子类内部可以访问其从基类继承而来的
**protected**
方法 - 子类内部不能访问基类实例的
**protected**
方法。 ```java class A { protected void x() {
} }
- 子类内部可以访问其从基类继承而来的
//B与Object在不同的包,与A在同一个包 class B { public void f() throws CloneNotSupportedException { // protected成员对本包所有类可见 A a = new A(); a.x(); //通过编译,因为protected对同一包内的类可见
//子类与基类不在同一包中,子类可以访问其从基类继承而来的protected方法,
//但不能访问基类实例的protected方法。
super.clone(); //通过编译
Object object = new Object();
object.clone(); //!!!不通过编译
}
}
- Object类的`clone()`方法被声明为`protected`
- **应用**
如果父类的某方法只想让其子类可见,则将其声明为`protected`
**3.5 访问控制修饰符总结**
- **成员声明访问控制修饰符后的可见范围表**
| **修饰符** | **当前类** | **同一包内** | **子孙类(同一包内)** | **子孙类(不同包内)** | **其他包** |
| --- | --- | --- | --- | --- | --- |
| **default** | √ | √ | √ | <br /> | <br /> |
| **public** | √ | √ | √ | √ | √ |
| **private** | √ | <br /> | <br /> | <br /> | <br /> |
| **protected** | √ | √ | √ | √/× | <br /> |
- **访问控制符修饰对象表**
| **修饰符** | **类** | **方法** | **字段** | **注意** |
| --- | --- | --- | --- | --- |
| **default** | √ | √ | √ | <br /> |
| **public** | √ | √ | √ | <br /> |
| **private** | <br /> | √ | √ | 不能修饰类(内部类除外) |
| **protected** | <br /> | √ | √ | 不能修饰类(内部类除外),接口及接口的字段和方法不能声明为protected) |
- **访问控制修饰符和继承**
请注意以下方法继承的规则:
- **父类中声明为**`**public**`**的方法在子类中也必须为**`**public**`
- **父类中声明为**`**protected**`**的方法在子类中要么声明为**`**protected**`**,要么声明为**`**public**`**,不能声明为**`**private**`
- **父类中声明为**`**private**`**的方法,不能够被继承**
<a name="p8Zdo"></a>
### 4 非访问控制修饰符 - final、static、abstract...
- 为了实现一些其他的功能,Java提供了6种非访问修饰符
1. `final`
1. `static`
1. `abstract`
1. `synchronized`
1. `volatile `
1. `transient`
<a name="patZC"></a>
#### 4.1 final修饰符
> 与C++不同,Java使用`final`修饰常量而不是`const`。但`const`是Java保留的关键字
- **修饰对象:**类、方法、字段
- 修饰的**类不能够被继承**
- 修饰的**方法不能被继承类重写**
- 修饰的**字段为常量,是不可修改的**
**final字段(常量)**
- 关键字`final`常用于指示**常量**(Java没有`const`),表示字段只能被赋值一次,**一旦赋值之后,就不能再更改了。**
- 常量名常用**全大写**
- **常量的初始化**
- **常量在方法中声明时,不需要马上初始化,但只有一次赋值机会**
- 当常量作为类成员时,**必须在构造对象时初始化**,即**构造器执行之后,这个字段的值已经设置**,并且**以后无法再修改这个字段**
- 对于每个类来说,最好不要有公共字段。然而,**公共常量(即public final字段)**却没问题,因为无法改变。
- **应用**
`final`修饰符通常用于类型为**基本类型**或者**不可变类(没有更改器方法的类,如String)的字段**
- `final`对于**基本类型字段**的作用为**严格意义上的不可修改**。
- `final`对于**对象变量(对象引用)**的作用则为**表示对象引用不会再指示另一个不同的对象**。
如果修饰的对象变量是**不可变类**,那么该`final`对象变量也是严格意义上的不可修改。<br />如果修饰的对象变量是**可变类**,那么该`**final**`**对象变量是可以改变的**。
```java
final int A = 1; //绝对不可变
final String STR = "Good"; //绝对不可变
//ARRAY不可引用其他对象,但其引用的对象{1, 2, 3}是可变的
final int[] ARRAY = new int[]{1, 2, 3};
ARRAY[0] = 5;
System.out.print(ARRAY[0]); //输出5
- 静态常量/类常量
final
修饰符通常和static
修饰符一起使用可以来创建类常量(因为常量不可修改,所以不需要让每个类实例去创建一个常量的副本),如
static final String TITLE = "Manager";
final方法
- 父类中的
**final**
方法可以被子类继承,但是不能被子类重写。 - 声明
final
方法的主要目的是防止该方法的内容被修改。
final类
final
类不能被继承,没有类能够继承final
类的任何特性。
4.2 static修饰符
- 修饰对象:方法、字段
static
关键字指示属于类而不属于任何类实例的字段和方法
对静态字段和方法的访问可以直接使用类名.字段名
和类名.方法名
的方式访问。
静态字段(类字段)
- 在变量一节也有关于静态字段的介绍。
static
关键字用来声明独立于对象的静态字段- 如果将一个字段定义为static,则每个类的实例共享这一个static字段。
对于非静态的实例字段,每个类的实例都有其自己的一个副本。
- 即使没有类的对象,静态字段也存在,即静态字段属于类,而不属于任何单个的对象,所以静态字段也被称为类字段。
- 类字段都可以通过
类名.字段名
直接访问,一般情况下- 静态变量访问类型为
private
, - 静态常量的访问类型为
public
。
- 静态变量访问类型为
- 静态常量(
**static final**
)
静态变量使用得比较少,但静态常量却很常用,例如java.lang.Math
中定义的PI静态常量
public class Math {
...
public static final double PI = 3.1415926535;
...
}
- 局部变量不能被声明为
static
。
静态方法
static
关键字用来声明独立于对象的静态方法。- 静态方法不能直接访问类的非静态字段(实例字段)也不能调用非静态方法
但是,静态方法可以访问静态字段以及调用静态方法。
- **在静态方法中新建对象,可以通过对象访问实例字段**
- 静态方法从参数列表和静态字段得到数据,然后计算这些数据。
- 使用对象调用静态方法是可以的,但是方法的运行与对象毫无关系,所以还是建议使用类名调用静态方法。
- 子类可以继承父类的static方法,并且也可以使用
**子类名.方法**
的形式调用静态方法
静态工厂方法(factory method)
- 静态方法还有另外一种常见的用途,类似LocalDate和NumberFormat等类使用静态工厂方法来构造对象。
如LocalDate.now()
、LocalDate.of()
以及NumberFormat.getCurrencyInstance()
、NumberFormat.getPercentInstance()
- 静态工厂方法定义
对于一个类来说,为了获得一个自身的实例,最常用的方法就是提供一个公有的构造器。除了这种使用构造器的方法之外,对于单个类来说,我们可以定义静态工厂方法来获取自身类的一个实例。
我们通过静态工厂方法来代替构造器,首先需要知道的是静态工厂方法只是一个“普通的方法”。的确,我们将具有“返回这个对象的一个实例”特点的静态方法叫做静态工厂方法,它在理论上与其他方法并没有什么不同。但是也正是因为他是一种“普通的方法”,它具有很多构造器不具备的优点。
- 静态方法的优点
- 静态工厂方法可以有不同的名字
对于普通的构造器来说,通过参数来对类中的不同属性赋值,然后返回一个这个类的相应实例。
但在有些时候,如果我们想获得有些差别的类实例,唯一可以采用的方法是通过不同的构造器参数列表来区分不同的实例。但是这并不是一个好主意,因为有的时候,仅仅是构造器参数列表的不同,让我们很难去记住到底哪个构造对应的是哪个实例。
这个时候可以考虑使用静态工厂方法,为不同的构造方法来起不同的名字来区分不同功能的实例化,而返回的都是一个this,这样类似构造器的操作让不同的实例可以被更加容易的区分。
- 静态工厂方法可以返回一个现有的实例
每当我们调用构造器,使用new来初始化一个对象时,无疑是在堆上创建了一个新的对象。而有些不可变的类、不希望被实例出多个对象的类,不用也不必每次都创建一个对象。通过静态工厂方法,我们可以直接返回一个我们早已创建好的对象,对于有些不可变的类,比如基本类型包装类,这样做可以极大地节省我们的开销。
对于单例模式的类,只允许每个类中存在一个已经实例化的对象。对于单例模式来说,构造器都会被声明为private,我们不能也无法构造一个对象,只能通过静态工厂方法来获取他的对象。这种手法在很多很多包中都有体现,尤其是那些比较重量级的类(其实我们也恰好总喜欢把这种变量其名为factory),更需要这种手法来操作对象。
- 静态工厂方法可以构造任意类型对象
使用构造器时,无法改变所构造对象的类型。
有时我们可能需要使用某一类的构造器来生成另外一个类的实例,此时就需要使用静态工厂方法。
- 静态工厂方法可以作为视图返回子类型
视图这个名词有很多解释,在这里视图的解释是Java集合框架中的视图方法——通过返回一个List(或者其他具体类型的祖先类,Set,Map等)对象,让那些List的子类都可以来调用这个方法。
- 静态工厂方法使用实例
对于NumberFormat
类来说,不利用构造器完成这些操作的原因有两个:
- 无法命名构造器。构造器的名字必须与类名相同,但这里希望有两个不同的名字,分别得到货币实例和百分比实例
- 使用构造器时,无法改变所构造对象的类型,而工厂方法实际上将返回
DecimalFormat
类的对象,这是NumberFormat
的一个子类。
4.3 abstract修饰符
- 修饰对象:类、方法
abstract
用来创建抽象类和抽象方法。
抽象类
- 抽象类不能用来实例化对象,声明抽象类的唯一目的是为了将来对该类进行扩充(作为基类)。
- 一个类不能同时被
**abstract**
和**final**
修饰。 - 如果一个类包含抽象方法,那么该类一定要声明为抽象类,否则将出现编译错误。
但是抽象类可以包含抽象方法和非抽象方法。
抽象方法
- 抽象方法是一种没有任何实现的方法,该方法的的具体实现由子类提供(或运行时由匿名类实现)。 ```java public abstract class SuperClass{ abstract void m(); //声明抽象方法m() }
class SubClass extends SuperClass{ //实现抽象方法m() void m(){ ……… } }
- 抽象方法不能被声明成`final`或`static`。
- **任何继承抽象类的子类必须实现父类的所有抽象方法,除非该子类也是抽象类**
- 抽象方法的**声明以分号结尾**,例如:`public abstract sample();`。
> Java不能像C++一样,将类成员方法的声明和实现分开写,只有抽象类可以声明方法,由其子类或匿名类实现
<a name="fKpu9"></a>
#### 4.4 synchronized修饰符和volatile修饰符
这两个修饰符主要用于线程的编程。
- **synchronized修饰符**
**修饰对象:**方法<br />`synchronized`关键字声明的方法**同一时间只能被一个线程访问**。
- **volatile修饰符**
**修饰对象:**字段
- `volatile`修饰的成员变量在**每次被线程访问时,都强制从共享内存中重新读取该成员变量的值**。
而且,**当成员变量发生变化时,会强制线程将变化值回写到共享内存。**<br />这样在任何时刻,**多个不同的线程总是看到某个成员变量的同一个值**。
- 一个`volatile`对象引用可能是 null。
- **实例**
```java
public class MyRunnable implements Runnable
{
private volatile boolean active;
public void run()
{
active = true;
while (active) // *
{
... ...
}
}
public void stop()
{
active = false; // #
}
}
- 通常情况下,在一个线程调用`run()`方法(在Runnable开启的线程),在另一个线程调用`stop()`方法。
- **上述代码中,**在`*`标行,缓冲区的`active`值被使用,如果字段`activate`没有被声明为`volatile`,那么即使在另一个线程调用`stop()`方法,`*`标行的循环也不会停止,但是以上代码中我们使用了`volatile`修饰`active`,所以该循环会停止。
4.5 transient修饰符
… …
4.6 非访问控制修饰符总结
- 非访问控制修饰符修饰对象及互斥关系表
| 修饰符 | 类 | 方法 | 字段 | 互斥修饰符 |
| —- | —- | —- | —- | —- |
| final | √ | √ | √ | abstract |
| static |
| √ | √ | abstract | | abstract | √ | √ |
| final、static | | synchronized |
| √ |
|
| | volatile |
|
| √ |
|
5 构造器
构造器定义 ```java class Student{ private String s_name;
//构造器 Student(String name){
this.s_name = name;
} }
public class MyClass { public static void main(String[] args) { Student s = new Student(“崔奕宸”); //使用构造器构造一个Student实例 } }
- 类构造对象时调用的方法,主要用来实例化对象,并且对对象进行初始化。
- 构造器通常也叫**构造方法、构造函数。**
- 构造器和普通函数一样,可以有0个或者多个参数
- **每个类可以有一个以上的构造器**
- 构造器总是伴随着`new`操作符一起使用。
- **构造器与一般函数的区别**
- **声明返回值类型**
一般函数必须声明返回值的类型<br />**构造器则不需要也不能声明返回值的类型**,当然**更不能写**`**return**`**语句**
- **作用**
一般函数用于定义对象应该具备的功能。<br />构造函数用于定义对象在调用功能之前,在建立时,应该具备的一些内容。也就是对象的初始化内容。
- **执行时间**
一般函数是对象建立后,当对象调用该函数时才会执行。<br />构造函数是在对象建立时就由JVM自动调用并执行, 给对象初始化。
- **执行次数**
一般函数可以被对象多次调用,<br />**构造函数只在创建对象时调用,且只能执行一次**,对象构建后,不能再试图使用构造函数初始化对象状态
- **命名规则**
**构造函数的函数名要与类名一样**<br />一般函数只要符合标识符的命名规则即可。
- **构造器的调用**
```java
Student s = new Student("崔奕宸");
- 构造器必须伴随着
new
操作符一起调用 - 调用构造器时,直接直接使用构造器名即可,不能采用
类名.构造器()
的形式 - 在类中,一般函数不能调用本类的构造函数,只有本类的构造函数才能调用本类的构造函数。
- 构造器调用本类另一个构造器
关键字this指示一个方法的隐式参数,在构造器中,它还有另外一个含义。
如果构造器的第一个语句(必须是第一个语句)形如this(…)
,那么这个构造器将调用同一个类的另一个构造器。
public Employee(String name, double salary) {
this(); //call Employee()
this.name = name;
this.salary = salary;
}
C++不允许构造器调用构造器。
- Java有两类构造器
- 无参数构造器
- 很多类都包含一个无参数的构造器,将对象的状态设置为适当的默认值。
- 一个类只能有一个无参数构造器
- 默认构造器
- 无参数构造器
默认构造函数是一个特殊的无参数构造器。
当类中没有定义构造函数时,系统会指定给该类加上一个空参数的不执行任何内容构造函数,且该空构造函数是隐藏不见的,这就是类中默认的构造函数。
- 如果类中自定义了构造函数,这时默认的构造函数就没有了。
- 有参数构造器
有参构造函数,在new对象时,将实参值传给实例字段,相当于完成set()
方法功能。
如果类中提供了至少一个构造器,但没有提供无参数的构造器,那么构造对象时如果不提供参数就是不合法的。
- 构造函数重载
构造函数也是函数的一种,同样具备函数的重载(Overloding)特性。
在一个类中可以定义多个构造函数,以进行不同的初始化。
class Employee {
private String name;
private double salary;
private LocalDate hireDay;
//无参数构造函数
public Employee() {
name = null;
salary = 0;
hireDay = null;
}
//有参数构造函数
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
hireDay = LocalDate.now();
}
//有参数构造函数
public Employee(String name, double salary, int year, int month, int day) {
this.name = name;
this.salary = salary;
hireDay = LocalDate.of(year, month, day);
}
}
- 析构器
- Java会自动完成垃圾回收,不需要人工回收内存,所有Java不支持析构器。
- 某些对象使用了内存之外的其他资源,当资源不再使用时,其回收和再利用就十分重要。
- 如果一个资源使用完就需要立即关闭,那么需要使用
close()
方法来进行清理工作。
- 如果一个资源使用完就需要立即关闭,那么需要使用
6 初始化实例字段
- 初始化实例字段的方法有以下四种:
- 在构造器中设置值
- 默认字段初始化
- 显式字段初始化,在字段声明时直接初始化
- 使用初始化块赋值
上一节以及了解了如何在构造器中设置字段的值,本节主要陈述另外三种方法
- 默认字段初始化
- 如果在构造器中没有显示地为某个字段设置初值,那么它就会被自动地赋为默认值:
数值为**0**
、布尔值为**false**
、对象引用为**null**
。
- 依赖系统给定的默认值是不好的编程习惯。
- 显式字段初始化
可以在字段声明时直接赋值。
class Employee
{
private String name = "";
private LocalDate hireDay = null;
...
}
- 为每个实例字段设置一个有意义的初值是一个好主意。
- 如果一个类的多个构造器都希望把某个特定的实例字段设置为同一个值,这个方法就特别有用。
C++不能直接初始化类的实例字段,所有字段必须在构造器中设置。
- 初始化块
在类中紧挨实例字段声明的部分,可以包含任意多个初始化块,只要构造这个类的对象,这些块就会被执行。
public class Employee {
private String name;
private LocalDate hireDay;
public double salary;
public static int nextId = 0;
//初始化块
{
nextId++; //每次新建对象,nextId都将自动增加1
}
public Employee() {
name = "";
salary = 0;
hireDay = LocalDate.now();
}
public Employee(String name, double salary) {
this(); //call Employee()
this.name = name;
this.salary = salary;
}
}
- 无论使用哪个构造器构造对象,都首先运行初始化块,然后才运行构造器的主体部分。
- 初始化块最好放在字段定义后,否则会出现一些小问题。
- 初始化块的作用
对所有对象进行统一初始化,实际上就是将所有构造方法中共有的初始化语句统一起来执行。
- 这种机制并不常见,通常会直接将初始化代码放在构造器中。
- 静态的初始化块
如果类的静态字段需要很复杂的初始化代码,那么可以使用静态的初始化块(类第一次加载时执行的代码),在类第一次加载的时候,对静态字段进行初始化。
class Employee {
private String name;
public double salary;
public static int nextId = 0;
//使用静态初始化,给nextId赋一个随机初始值
static {
Random generator = new Random();
nextId = generator.nextInt(10000);
}
Employee() {
nextId++;
}
public Employee(String name, double salary) {
this();
this.name = name;
this.salary = salary;
}
}
7 方法的调用与参数
1 按值调用与按引用调用
按值调用表示方法接收的是调用者提供的值,而按引用调用表示方法接收的是调用者提供的变量地址。
- 方法可以修改按引用传递的变量的值,而不能修改按值传递的变量的值。
- C++支持按引用调用,参数标有&符号的表示引用参数。
- Java语言总是采用按值调用,即方法得到的是所有参数值的一个副本,不能修改传递给它的任何参数变量的内容。
Java的按值调用
一个方法不可能修改基本数据类型,而对象引用作为参数则不是严格意义上的不可改变(这里类似于静态对象是否严格可变的概念)。
- 参数是对象引用的一个副本,但是参数与原来的对象变量引用的是同一个对象。如果该对象是可变类的一个实例,则参数调用更改器方法即可改变实际对象的状态。
- Java中对方法参数改变与否的总结
- 方法不能修改基本数据类型的参数
- 方法可以改变对象参数(可变类)的状态。
- 方法不能让一个对象参数引用一个新的对象(否则对象就是按引用传递了)。
2 隐式参数和显式参数
- 显式参数:方法中明确声明的参数,位于方法名后面括号中的值。
隐式参数:未在方法中声明,但却动态影响到程序运行的参数,称为隐式参数。实际上隐式参数就是被类的方法调用的实例字段。
实例
class A {
private double salary;
public void ariseSalary(double rate) { //显式参数:rate
double up = salary * rate; //隐式参数salary
salary += up; //隐式参数salary
}
}
- Java不能设置默认参数
3 this指针
- this指针定义
- this指针用于类的非静态方法中,其指代的是类的对象实例自己
this指针区分隐式参数和显式参数
- 在每一个方法中,关键字this用于指示隐式参数,Java默认省略this指针。
this如果不省略,可以将实例字段/类字段与局部字段明显地区分开来,如上述实例中的函数,可以使用this指针将salary和局部字段rate、up区分开来。
public void ariseSalary(double rate) {
double up = this.salary * rate;
this.salary += up;
}
注意,如果实力字段和局部字段重名,则必须使用this指针将它们区分开来。
4 参数命名
当参数名和实例字段/类字段存在冲突时,我们通常使用两种方法为参数命名
在参数前加前缀“a”
class Employee {
private String name;
private double salary;
public Employee(String aName, double aSalary) {
name = aName;
salary= aSalary;
}
}
参数名与实例字段/类字段同名,使用this指针区分
class Employee {
private String name;
private double salary;
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}
}
5 内部类
- 内部类定义
在Java中,可以将一个类定义在另一个类里面或者一个方法里面,这样的类称为内部类。
内部类包括四种:成员内部类、局部内部类、匿名内部类和静态内部类。
1 成员内部类
定义在另一个类内部的类称为成员内部类
class Circle {
//成员内部类Draw
class Draw { }
}
- Draw是类Circle的一个成员,Circle称为外部类
外部类与内部类的互相访问
- 成员内部类可以直接使用成员名访问外部类的成员(包括
private
成员),但外部类则不能直接使用成员名访问内部类成员。
- 成员内部类可以直接使用成员名访问外部类的成员(包括
在外部类中必须先创建一个成员内部类的对象,再通过指向这个对象的引用来访问内部类成员(包括private
成员)。
- 当成员内部类拥有和外部类同名的成员变量或者方法时,默认情况下访问的是成员内部类的成员。如果要访问外部类的同名成员,需要以下面的形式进行访问
外部类.this.成员变量
外部类.this.成员变量
- 创建内部类实例
成员内部类是依附外部类而存在的,也就是说,如果要创建成员内部类的对象,前提是必须存在一个外部类的对象。
创建成员内部类对象的一般有两种方式:
public class MyClass {
public static void main(String args[]) {
//第一种方式:
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner(); //通过Outer对象来创建
//第二种方式:
Outer outer2 = new Outer();
Outer.Inner inner1 =
outer2.getInnerInstance();//利用Outer类的方法获取成员内部类
}
}
class Outer {
private Inner inner = null;
public Inner getInnerInstance() {
if (inner == null)
inner = new Inner();
return inner;
}
class Inner { }
}
- 内部类的访问权限
内部类可以被private
、protected
、public
或default
修饰,而外部类只能被public
和default
修饰。因为成员内部类看起来像是外部类的一个成员,所以可以像类的成员一样拥有多种权限修饰。
2 局部内部类
定义在一个方法内部或一个作用域内部的类称为局部内部类
class People { }
class Man {
public People getWoman() {
//局部内部类Woman
class Woman extends People { }
return new Woman();
}
}
局部内部类的特点
- 局部内部类同样可以直接通过成员名访问外部类的成员(包括
**private**
成员) - 局部内部类就像是方法里面的一个局部变量一样,因此局部内部类在方法外是不可见的,在外部类中不能创建、访问局部内部类。
- 仅限于方法内或者作用域内可以创建、访问局部内部类实例
- 局部内部类同样可以直接通过成员名访问外部类的成员(包括
方法内不能直接通过成员名访问局部内部类成员,但是可以访问局部内部类的**private**
成员
- 包含局部内部类的方法不能将局部内部类作为返回值类型
- 局部内部类像局部变量一样,因此不能被访问权限修饰符和
static
修饰; 局部内部类可以访问包含他的方法中的局部变量,但是访问的局部变量必须有
**final**
修饰public class MyClass {
public static void main(String args[]) {
MyClass myClass = new MyClass();
myClass.a(); //out: 崔奕宸
}
void a() {
final String name = "崔奕宸";
class Inner {
private String aName = name; //访问方法的局部变量
}
Inner inner = new Inner();
System.out.println(inner.aName); //访问局部内部类的private成员
}
}
3 匿名内部类
- 匿名内部类定义
匿名内部类与局部内部类很相似,不同的是匿名内部类没有类名,而且在定义一个匿名内部类的同时对其进行实例化。
- 定义匿名内部类
匿名类是一个表达式,匿名类表达式的语法就类似于调用一个类的构建函数(new A()
),除些之外,还包含了一个代码块,在代码块中完成类的定义
匿名表达式包含以下部分
- 操作符
new
- 因为匿名类没有名字,因此
**new**
操作符后要跟一个要实现的接口或要继承的类 - 一对括号
- 如果是匿名子类,且父类构造器有参数,则要在括号内传入实参;
- 如果是实现一个接口,只需要一对空括号即可;
- 一段被
{ }
括起来的类声明主体; 末尾的
**;**
号(因为匿名类的声明是一个表达式,是语句的一部分,因此要以分号结尾)。interface HelloWorld {
void greet();
void greetSomeone(String someone);
}
public class MyClass {
public static void main(String args[]) {
MyClass myClass = new MyClass();
myClass.sayHello();
System.out.println();
myClass.bird.printAnimalName();
}
public void sayHello() {
//局部类EnglishGreeting实现了HelloWorld接口,先定义,再实例化
class EnglishGreeting implements HelloWorld {
public void greet() { greetSomeone("world"); }
public void greetSomeone(String someone) {
System.out.println("Hello " + someone);
}
}
HelloWorld englishGreeting = new EnglishGreeting();
//匿名类实现HelloWorld接口,定义的同时实例化
HelloWorld chineseGreeting = new HelloWorld() {
public void greet() {
greetSomeone("世界");
}
public void greetSomeone(String someone) {
System.out.println("你好" + someone);
}
};
englishGreeting.greet();
chineseGreeting.greetSomeone("崔奕宸");
}
//成员内部类Animal
class Animal {
private String name;
public Animal(String name) {
this.name = name;
}
public void printAnimalName() {
System.out.println(bird.name);
}
}
//匿名子类,继承自Animal类
Animal bird = new Animal("布谷鸟") {
@Override
public void printAnimalName() {
System.out.println("匿名内部成员类bird");
super.printAnimalName();
}
};
}
- 匿名内部类的特点
- 匿名内部类不能有访问修饰符和
**static**
修饰符 - 匿名内部类是唯一没有构造器的类
- 如果某个局部类只需要用一次,就可以使用匿名内部类(一般在传参的时候使用)
- 匿名内部类一般用于接口回调
- 匿名内部类不能有访问修饰符和
4 静态内部类
- 静态内部类
静态内部类也是定义在另一个类里面的类,与成员内部类相比,静态内部类多了一个关键字static
。
public class MyClass {
public static void main(String args[]) {
//不用创建外部类对象就可以直接访问内部类
Inner inner = new Inner();
Outer.Inner2 inner2 = new Outer.Inner2();
}
static class Inner { }
}
class Outer {
static class Inner2 { }
}
- 静态内部类特点
- 静态内部类是不需要依赖于外部类的,这点和类的静态成员属性有点类似
- 静态内部类不能使用外部类的非
static
成员 - 在没有外部类的对象的情况下,可以直接创建静态内部类的对象
5 使用内部类的好处
主要有以下四点原因:
- 每个内部类都能独立的继承一个类,内部类完善了Java的多继承机制。
- 方便将存在一定逻辑关系的类组织在一起,又可以对外界隐藏
- 方便编写事件驱动程序
- 方便编写线程代码
6 内部类相关问题
获取内部类实例 ```java class Test{ public static void main(String[] args){
//初始化Bean1
Test test = new Test();
Test.Bean1 bean1 = test.new Bean1();
bean1.I++;
//初始化Bean2
Bean2 bean2 = new Bean2();
bean2.J++;
//初始化Bean3
Bean bean = new Bean();
Bean.Bean3 bean3 = bean.new Bean3();
bean3.K++;
//初始化Bean4
Bean.Bean4 bean4 = new Bean.Bean4();
bean4.Q++;
}
class Bean1{ public int I = 0;} static class Bean2{ public int J = 0;} }
class Bean{ class Bean3{ public int K = 0;} static class Bean4{ public int Q = 0;} }
- 从前面可知,对于成员内部类,必须先产生外部类的实例化对象,才能产生内部类的实例化对象。而静态内部类不用产生外部类的实例化对象即可产生内部类的实例化对象。
- 创建静态内部类对象的一般形式为:
```java
外部类类名.内部类类名 xxx = new 外部类类名.内部类类名()
创建成员内部类对象的一般形式为:
外部类类名.内部类类名 xxx = 外部类对象名.new 内部类类名()
- 访问各类变量
```java
public class MyClass {
public static void main(String args[]) {
} }Outer outer = new Outer();
outer.new Inner().print();
class Outer { private int a = 1;
class Inner {
private int a = 2;
public void print() {
int a = 3;
System.out.println("局部变量:" + a);
System.out.println("内部类变量:" + this.a);
System.out.println("外部类变量:" + Outer.this.a);
}
}
}
上述代码输出
局部变量:3 内部类变量:2 外部类变量:1
<a name="yOWCh"></a>
## 6 枚举类型
- **枚举定义**
**枚举是一个特殊的类**,一般表示一组常量
- **枚举的作用**
枚举类型用于**控制枚举类型变量的取值在一个有限的集合内**<br />使用枚举可以减少代码中的bug
- **枚举的声明和使用**
- 枚举使用`enum`关键字来定义,枚举内的成员(常量)使用逗号`,`来分隔
- **枚举可以声明在类的外部或内部**
- 可以把枚举内的成员认为是**类常量**(实际上也确实如此),因此**可以直接采用**`**枚举名.常量名**`**的形式访问枚举内的常量**。
- **枚举内常量的类型是定义它们的枚举类**,而`**常量名.toString()**`**将直接返回常量名**(原因见深度剖析)
- **实例**
```java
enum JuiceSize {
SMALL, MEDIUM, LARGE
}
public class MyClass {
public static void main(String[] args) {
JuiceSize juice = JuiceSize.LARGE; //常量的类型是其枚举的名字
System.out.println(juice); //out: LARGE
}
}
- 枚举常应用于
**switch**
语句中 ```java enum Color { RED, GREEN, BLUE; }
public class MyClass { public static void main(String[] args) { Color myVar = Color.BLUE;
switch (myVar) {
case RED:
System.out.println("红色");
break;
case GREEN:
System.out.println("绿色");
break;
case BLUE:
System.out.println("蓝色");
break;
}
}
}
- 可以看到,在`case`语句后可以直接跟枚举内的常量,如`RED`,而不需要使用如`Color.Red`的形式
- 如果想遍历枚举的所有常量,可以使用静态方法`values()`
<a name="w82T5"></a>
### 深入剖析
- **枚举的实现**
对于枚举Color
```java
enum Color{
RED, GREEN, BLUE;
}
其内部实现相当于
class Color{
public static final Color RED = new Color();
public static final Color BLUE = new Color();
public static final Color GREEN = new Color();
}
- 因此枚举类内的成员都是类常量,且类型都是枚举定义它们的枚举类
enum
声明的枚举默认继承了**java.lang.Enum**
类,并实现了java.lang.Seriablizable
和java.lang.Comparable
两个接口。Enum类重写了
**toString()**
方法,因此直接打印枚举内的常量会输出常量的名字,源码如下:public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable {
private final String name;
private final int ordinal;
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
public String toString() {
return name;
}
...
}
###Enum类方法(java.lang包)
静态方法
**Enum[] Enum.values()**
返回枚举类中所有值
该方法实际上并不是Enum类的,但是可以通过自己定义的枚举调用,如Color.values()
动态方法
- 这里动态方法实际上是可以通过枚举内的成员调用方法
**int enum.ordinal()**
返回枚举常量在枚举声明中的位置