Chap 1 —— Java入门
JVM、JRE、JDK的关系
- JVM是java的运行环境之一
- JRE = JVM + 运行时所需的核心类库
- JDK = JRE + java开发工具
要想运行一个已有的java程序,只需安装JRE即可;要想开发一个java程序,就需要安装JDK
注意:JVM本身不具有跨平台性,不同操作系统下需要安装不同版本的JVM
java开发步骤
-
编写java源程序
-
编译java源程序
使用javac.exe编译源程序,编译java源文件得到字节码文件
在保存源文件时,必须将保存类型选择为“所有文件”,并将“编码”选择为ANSI或UTF-8(不可以选择“带BOM的UTF-8”)
ANSI编码在不同的系统中代表着不同的编码。在Windows简体中文系统下代表GBK编码;在Windows日文系统下代表JIS编码
如果保存java源文件时选择的编码是ANSI,那么可以直接使用javac编译源文件
如果保存java源文件时选择的编码是UTF-8,那么使用javac编译源文件时必须显式用-encoding参数,告知编译器使用怎样的编码解析、编译源文件,即-encoding给出的值必须和源文件的编码相同(不显式使用-encoding参数,那么默认该参数的值是GBK)
javac -encoding utf-8 Hello.java -
运行java程序
使用java.exe解释并执行字节码文件
java应用程序总是从主类的main方法开始执行的,因此,需要进入主类字节码所在的目录,在该目录下使用
java Hello命令来运行主类的字节码
注意:
- 如果一个源文件中有多个类,那么只能有一个类是public类;如果源程序中有一个类是public类,那么源文件的名字必须与这个类的名字完全相同
- 一个java应用程序至少要有一个类含有
public static void main(String args[])方法,这个类称为应用程序的主类
Chap2 —— 基本数据类型与数组
注意:在java中,true、false和null虽然不是关键字,但也不能将它们作为标识符使用(它们是作为变量的值使用的)
基本数据类型
java中有8种基本数据类型:boolean、byte、short、int、long、float、double、char
这8种基本数据类型习惯上可分为以下四大类型:
- 逻辑类型:boolean
- 整数类型:byte、short、int、long
- 字符类型:char
- 浮点类型:float、double
整数类型
int型常量:int型常量共有4种表示方法:
- 十进制: 123,6000(十进制,无前缀)
- 八进制: 077(八进制,数字零做前缀)
- 十六进制: 0x3ABC(十六进制,0x或 0X做前缀)
- 二进制:0b111(用0b或0B做前缀)
int型变量:int 变量名 = int型常量
对于int型变量,占4个字节
byte型常量:一定范围内的int型常量可以直接赋值给byte型变量
对于byte型变量,占1个字节
short型常量:和byte型类似,Java中也不存在short型常量的表示法,但可以把一定范围内的int型常量直接赋值给short型变量
对于short型变量,占2个字节
long 型常量:long型常量用后缀L来表示:
-
十进制:108L
-
八进制:07123L
-
十六进制:0x3ABCL
例如: long width=12L, height=2005L, length;
long型变量:long 变量名 = long型常量
对于long型变量,占8个字节
字符类型
对于char型变量,占2个字节
如果要观察一个字符在Unicode表中的顺序,可以使用int类型转换,例如(int)'A'。如果要得到一个0~65535的数所代表的 Unicode 表中相应位置上的字符,可以使用char型类型转换,例如(char)65
浮点类型
float型常量:453.54F(小数表示法),2e40f(2乘10的40次方,指数表示法)
float型变量:float 变量名 = float型常量
精度:float变量在存储时保留8位有效数字
对于float型变量,占4个字节
注意:float常量后面必须要有后缀f或F
double型常量:238.539d,231.987(小数表示法),1e-90(1乘10的-90次方,指数表示法)
double型变量:double 变量名 = double型常量
对于double型变量,占8个字节
精度:double变量在存储double型数据时保留16位有效数字,实际精度取决于具体数值
注意:对于double常量,后缀有“d”或“D”,但也允许省略后缀。一个使用小数表示法表示的数据,默认类型是double而不是float,因为float常量后面必须要有后缀“f”或“F”
数据类型转换
Java中数据的基本类型(不包括逻辑类型)按精度(级别)从“低”到“高”排列:
byte short char int long float double
1 2 2 4 8 4 8
类型转换规则:
-
当把级别低的变量的值赋给级别高的变量时,系统自动完成数据类型的转换
例如:float x = 100;
-
当把级别高的变量的值赋给级别低的变量时,必须使用显式类型转换运算。显式转换的格式:(类型名)要转换的值
例如:int x = (int)34.89;
-
当把一个int型常量赋值给一个byte和short型变量时,不可以超出这些变量的取值范围,如果超出则报错,不超出则在转换时需要进行显式类型转换运算
例如:byte b=128;(error) byte b=(byte)128;(ok) byte型变量取值范围为-128 ~ +127
数组
数组是相同类型的数据按顺序组成的一种复合数据类型,数组变量属于引用型变量
声明一维数组有下列两种格式:
-
数组的元素类型 数组名 [];
-
数组的元素类型 [] 数组名;
例如:float boy []; char [] cat;
声明二维数组有下列两种格式:
-
数组的元素类型 数组名 [][];
-
数组的元素类型 [][] 数组名;
例如:float a [][]; char [][] b;
为数组分配空间的格式如下:数组名 = new 数组元素的类型[数组元素的个数];
例如:boy = new float[4];
数组的声明和分配空间可以同时完成:float boy [] = new float[4];
通过这种方式给数组分配完空间后,系统会给数组的每个元素赋上一个默认值,例如:float型数组元素的默认值是0.0f
可以在此基础上单独给某一个元素赋上新值,如boy[0] = 1.1f;
可以在声明数组的同时给数组的每一个元素都设置初始值,例如:float boy[] = {1.1f , 2.2f};,该操作只能在和声明数组的同时进行,不能单独进行
工具类Arrays的Arrays.sort()方法可以给数组元素进行排序
Chap4 —— 类与对象
对象属性的默认初始化值
当变量作为类的成员使用时,即使没有进行初始化,java也会确保它获得一个默认值
| 数据类型 | 初始指 |
|---|---|
| byte | 0 |
| short | 0 |
| int | 0 |
| long | 0L |
| float | 0.0f |
| double | 0.0 |
| char | '\0' |
| boolean | false |
| 引用类型 | null |
全类名和简单类名
简单类名:不带包名的类名,如Person
全类名:包名.类,如com.cxl.ch4.demo01.Person
方法重载(Overload)
在同一个类中,允许存在多个同名方法,只要它们的参数个数或顺序或类型不同即可
注意:方法如果仅仅只是返回类型不同,那么并不算是方法重载
this关键字
this是java中的一个关键字,表示某个对象
this可以出现在实例方法和构造方法中,但不可以出现在类方法中
- this出现在类的构造方法中时,代表使用该构造方法所创建的对象
- this出现在实例方法中时,代表正在调用该实例方法的实例
this关键字的作用:
-
解决成员变量与局部变量同名的问题
-
在构造方法中调用构造方法
public class Person { String name; int age; public Person(String name) { this.name = name; } public Person(String name, int age) { this(name); // 调用同类的另一个构造器 this.age = age; } } -
this可以调用成员属性和成员方法
public class Test { public void display() { System.out.println("Displaying"); } public void anotherMethod() { this.display(); // 调用当前对象的display方法 } } -
当需要返回当前对象的引用时(链式编程)
public class Person { public Person clone() { // 假设这里只是简单地返回当前对象的引用作为示例 return this; // 返回当前对象的引用 } }
static关键字
使用static关键字修饰成员表示静态的含义,此时成员变量由对象层级提升为类层级,也就是在整个类中只有一份并被所有对象共享,该成员变量随着类的加载而准备就绪,与是否创建对象无关
static关键字修饰的成员可以使用 引用. 的方式访问(即静态变量可以通过 实例. 的方式进行使用),但更推荐 类名. 的方式
注意:
- 在非静态成员方法中既可以访问非静态的成员也可以访问静态的成员(成员包括成员变量和成员方法,且静态成员被所有对象共享)
- 在静态成员方法中只能访问静态成员而不能访问非静态成员
- static不能修饰局部变量
成员变量的初始化顺序
- 在类的内部,变量定义的先后顺序决定了初始化的顺序
- 即使变量定义散布于方法定义之间,它们仍旧会在任何方法(包括构造方法)被调用之前得到初始化
- 初始化的顺序是先“静态”对象(静态对象只被初始化一次),而后是“非静态”对象(创建一个实例就初始化一次)
权限修饰符
权限修饰符包括:public、protected、default、private
权限修饰符可以修饰类中的属性和方法(包括静态的成员)
| 修饰符名称 | 含义 | 本类 | 同包其他类 | 子类 | 同工程其他类 |
|---|---|---|---|---|---|
| private | 私有 | ✔️ | ❌ | ❌ | ❌ |
| default | 缺省 | ✔️ | ✔️ | ❌ | ❌ |
| protected | 受保护 | ✔️ | ✔️ | ✔️ | ❌ |
| public | 公共 | ✔️ | ✔️ | ✔️ | ✔️ |
注意:class关键字之前的权限修饰符只允许使用public,或者什么都不写(即default)
包装类
java中基本数据类型不面向对象,因此在设计类时为每个基本数据类型设计了一个对应的类进行代替
| 原始类型 | 包装类 |
|---|---|
| boolean | Boolean |
| byte | Byte |
| short | Short |
| char | Character |
| int | Integer |
| long | Long |
| float | Float |
| double | Double |
包装类的主要用途:
- 作为和基本数据类型对应的类类型存在,方便涉及到对象的操作
- 包装类中包含了每种基本数据类型的相关属性,如最大值、最小值等,以及相关的操作方法
Chap5 —— 子类与继承
super关键字
在子类中调用父类(祖先类)对象的成员变量,使用super.变量的方式
在子类中调用父类(祖先类)对象的成员方法,使用super.方法()的方式
方法重写(Override)
从父类中继承下来的方法功能不满足子类的需求时,就需要在子类中重新写一个和父类一模一样的方法来覆盖从父类中继承下来的版本,该方式就叫做方法的重写,或称方法的覆盖、方法的复写
方法重写的原则:
-
要求方法同名、参数列表相同以及返回值类型相同
-
要求方法的访问权限不能变小,可以相同或者变大
-
要求方法不能抛出更大的异常
子类重写的方法抛出的异常类型必须小于或等于父类中被重写的方法抛出的异常类型(即子类抛出的异常类型应该是父类抛出异常类型的子类或相同类型)。但是,子类重写的方法可以选择不抛出任何异常,即使父类方法抛出了异常
多层继承
class A extends B{ }
class B extends C{ }
class C extends Object{ }
A类可以同是拥有B和C、Object的成员,B能拥有C和Object的成员
A类中super调用的是B类成员,但如果B类没有相应成员,则调用C的成员
Object类是java中的祖先类,所有的类都是Object的直接子类或间接子类
final关键字
final的意思为最终,不可变
final是个修饰符,它可以用来修饰类,类的成员方法,以及变量
final修饰的类不可以被继承(不可以有子类),但是可以继承其他类
class Yy { }
final class Fu extends Yy{ } //可以继承Yy类
class Zi extends Fu{ } //错误,不能继承Fu类
final修饰的成员方法不可以被子类重写
final修饰的非静态成员变量,需要在对象创建完成前赋值,否则报错(final修饰的静态成员则必须在声明时赋值,即常量)。(当没有显式赋值时,多个构造方法的均需要为其赋值)
public class Demo {
final int M = 100;
final int N;
public Demo(){
N = 2016;
}
public Demo(int N){
this.N = N;
}
public void setN(int n){
N = n; //编译报错
}
}
public static final关键字共同修饰的成员变量可以表达常量的含义,常量的命名规范要求是所有字母都要大写,不同的单词之间采用下划线相连(常量必须在声明时就赋一个初始值)
public static final double PI = 3.14;
final修饰引用类型的变量,引用型变量的值为对象地址值,地址值不能更改,但是地址内的属性值可以修改
多态性
Java中多态性体现在2个方面:
-
静态多态性(编译时多态)
方法重载:编译期根据参数不同确定调用相应方法
-
动态多态性(运行时多态)
方法重写:运行时后期绑定子类重写的方法
动态多态性的存在前提:
- 必须有继承或者是接口实现
- 必须有方法的重写
多态的语法规则:
-
父类的对象引用指向子类的对象实体
父类 对象 = new 子类对象(); //多态写法
-
接口变量指向实现类的对象实体
接口 变量 = new 实现类对象();
对象调用方法时执行子类重写的方法,如果子类没有重写,则会调用从父类继承过来的方法
多态代码中访问成员变量时:
- 编译 : 父类中没有成员变量,编译失败
- 运行 : 运行父类中的成员变量
多态代码中访问成员方法时:
- 编译 : 父类中没有成员方法,编译失败
- 运行 : 运行子类的方法重写
注意:
- 成员方法编译看左边,运行看右边;成员变量都看左边
- 多态的程序中,不能调用子类的特有成员,只能调用子父类的共有成员,或父类特有的成员
引用类型的转换
向上转型:Animal animal = new Cat();
子类Cat类型自动提升为父类Animal类型,具备了多态性
上转型对象不能调用子类(Cat)特有的成员(因为具备了多态性),如果要访问的话就需要进行向下转型,即强制类型转换,将提升为Animal类型的Cat类型,再转回Cat类型
向下转型:Cat cat = (Cat)animal;
向下转型的对象会失去多态性
转型异常:发生ClassCastException类型转换异常是在进行类型的强制转换时发生的
注意:
- Dog不能转成Cat,Cat也不能转成Dog,如果将Dog和Cat进行互转就会发生编译错误,因为二者不构成继承体系
- 进行向下转型时,被转型的对象必须是由被转类型向上转型而来的,否则会发生转型异常
抽象类
没有方法体的方法应该定义为抽象方法
抽象方法不需要{},直接分号结束
使用关键字abstract来定义抽象方法:
权限修饰符 abstract 返回值类型 方法名称(参数列表);
抽象方法可以被修饰符public、protected、default修饰(private修饰抽象方法时会发生编译错误)
当一个类中含有抽象方法时,这个类就必须是抽象类,需要在关键字class前面使用abstract修饰
public abstract class 类名{
权限修饰符 abstract 返回值类型 方法名称(参数列表);
}
抽象类不能实例化对象,需要子类继承抽象类,并重写抽象方法,然后使用多态方式创建对象,调用方法时执行子类重写的方法
抽象类中允许定义成员变量(可以是静态的,也可以是非静态的)。可将成员变量私有修饰,并提供get/set方法,通过子类对象的继承进行使用
abstract class Animal{
private String name;
public abstract void makeSound();
public Animal() {
}
public Animal(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
class Cat extends Animal {
public Cat() {
super("猫"); // 调用抽象父类的有参构造
}
public void makeSound() {
System.out.println(super.getName() + " sound Wang!");
}
}
class Test {
public static void main(String[] args) {
Animal cat = new Cat();
cat.makeSound();
}
}
抽象类中有构造方法,不写有默认的无参构造方法
抽象类中,可以不定义抽象方法(没有抽象方法的抽象类依然不能直接创建实例对象),但如果有抽象方法存在,这个类必须是抽象类
当一个子类继承一个抽象类时,子类必须重写全部的抽象方法,否则子类依然还是抽象类
Chap6 —— 接口与实现
接口
Java的四大引用数据类型:class、interface、enum、annotation
类与接口是实现关系(is-like-a)
接口本质上是一个特殊的抽象类
接口所在的文件仍为.java文件,编译后,生成的依然还是.class文件
接口中成员定义具有固定格式:
-
成员变量:
public static final 数据类型 变量名 = 值; -
成员方法:
public abstract 返回值类型 方法名(参数列表);
接口的多态语法:
接口类型 变量名 = new 接口实现类();
在接口多态语法中,变量名调用方法,执行的是实现类重写的方法
接口回调:把实现某一接口的类创建的对象的引用赋给该接口声明的接口变量中,程序运行时,该接口变量就可以回调被实现类重写的方法
实现类实现接口,如果只重写一部分抽象方法,那么实现类就是一个抽象类
允许一个类同时实现多个接口:
class 类名 implements 接口1, 接口2 { }
一个类如果实现了多个接口,就需要重写多个接口中的所有抽象方法,否则该类就是抽象类
一个类如果实现了某个具有继承体系的接口,那么需要重写接口中以及接口继承过来的所有抽象方法,否则该类就是抽象类
interface 接口1 {
int a();
}
interface 接口2 extends 接口1 {
int b();
}
class Impl implements 接口2 {
@Override
public int a() {
return 0;
}
@Override
public int b() {
return 0;
}
}
接口的好处:
- 接口的出现扩展了功能
- 接口其实就是暴露出来的规则
- 接口的出现降低了耦合性,即实现类与实现类之间实现了解耦
友好接口:public修饰的接口就是友好接口,友好接口可以被同工程中的任何一个类实现
继承与实现
类和类之间是继承关系(单继承)is-a,且只能单继承,但是可以多层继承
类和接口之间是实现关系(多实现implements)is-like-a,可以单实现,也可以多实现,还可以在继承一个类的同时实现多个接口
接口和接口之间是继承关系,可以单继承,也可以多继承,一个接口可以同时继承多个接口,is-a
接口与抽象类的对比
相同点:
- 都位于继承的顶端,用于被其他类实现或继承
- 都不能直接实例化对象
- 都允许包含抽象方法,其子类都必须重写这些抽象方法,否则子类就是抽象类
不同点:
- 接口只能存在常量(public static final),不能出现变量;抽象类可以有常量、变量(变量可以是静态的,也可以是非静态的)
- 抽象类可以提供具体方法,接口则不能含有具体方法(即包含了方法体的方法)
- 一个类只能继承一个直接父类(可能是抽象类)却可以实现多个接口
- 抽象类是这个事物中应该具备的内容,继承体系是一种is a关系;接口是这个事物中的额外内容,继承体系是一种is like a关系
Chap7 —— 内部类与异常处理
内部类分为成员内部类和局部内部类
成员内部类
一个类定义在了另一个类的成员位置,这个内部类可以使用成员修饰符,如public、 static 、final、 private等
成员内部类可以直接使用外部类的成员,包括私有的成员
public class Outer {
private int num = 10;
public class Inner {
public void show() {
System.out.println(num);
}
}
public void method() {
Inner i = new Inner();
i.show();
}
}
外界创建非静态成员内部类的格式:外部类名.内部类名 对象名 = new 外部类名().new 内部类名();
Outer.Inner oi = new Outer().new Inner();
外界创建静态成员内部类的格式:外部类名.内部类名 对象名 = new 外部类名.内部类名();
Outer.Inner oi = new Outer.Inner();
class Outer {
static class Inner {
public void show(){
System.out.println("inner show");
}
public static void method(){
SYstem.out.println("inner method");
}
}
}
class Test{
public static void main(){
Outer.Inner oi = new Outer.Inner();
oi.show();
Outer.Inner.method();
}
}
成员内部类的同名变量调用:
class Outer {
int x = 2;
public class Inner {
int x = 1;
public void inner() {
int x = 0;
System.out.println(x);
System.out.println(this.x);
System.out.println(Outer.this.x);
}
}
public static void main(String[] args) {
Outer.Inner oi = new Outer().new Inner();
oi.inner();
}
}
内部类编译后也会生成class文件,文件名为 外部类$内部类.class
如:外部类Outer编译后会单独生成Outer.class,内部类Inner编译后会单独生成Outer$Inner.class
局部内部类
局部内部类是在方法中定义的类,不能使用权限修饰符、static修饰,但可以被final修饰
局部内部类的访问与访问方法中的局部变量相似,可通过调用方法间接进行访问
局部内部类,外界无法直接使用,只能是在包含局部内部类的外部类方法中,创建内部类对象,然后进行访问
局部内部类可以直接访问外部类的成员,也可以访问方法内的局部变量,但局部内部类访问的局部变量必须是被final修饰的,否则编译错误
匿名内部类
匿名内部类即没有名字的内部类,是为了简化代码书写而设计的
简化包括:
-
实现类,实现接口,重写方法,创建对象
new 接口() { // 方法重写... } -
子类继承父类,重写方法,创建对象
new 父类() { // 方法重写... }
匿名内部类使用的前提:必须有接口实现,或者是类的继承
定义的匿名内部类有两个含义:
- 临时定义某一指定类型的子类或某一接口的实现类
- 定义后即刻创建刚刚定义的这个子类的对象,或实现类的对象
匿名内部类直接调用方法:
interface Inter {
void method();
}
class Test(){
public static void main(String[] args){
new Inter(){
@Override
public void method(){
System.out.println("我是匿名内部类");
}
}.method(); // 直接调用方法
}
}
局部匿名内部类
作为形参的匿名内部类就是局部匿名内部类
class Test {
public void startThread() {
// 局部匿名内部类实现Runnable接口
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
// 线程执行的代码
System.out.println("线程运行中...");
}
});
thread.start();
}
public static void main(String[] args) {
Test test = new Test();
test.startThread();
}
}
Lambda表达式
interface Swimming {
void swim();
}
class Test{
public static void goSwimming(Swimming swimming){
swimming.swim();
}
public static void main(String[] args){
goSwimming(new Swimming(){
@Override
public void swim(){
System.out.println("swimming");
}
});
// 等价与下面的写法
goSwimming(() -> {
System.out.println("swimming")
});
}
}
Lambda表达式的使用前提:
- 使用Lambda必须要有接口
- 接口中有且仅有一个抽象方法
注意:Lambda必须配合接口进行使用,只有一个抽象方法的抽象类也是不能使用Lambda表达式的
Lambda与匿名内部类的区别:
-
匿名内部类:可以是接口,也可以是抽象类,还可以是具体类
Lambda表达式:只能是接口
-
如果接口中有且仅有一个抽象方法,可以使用Lambda表达式,也可以使用匿名内部类
如果接口中多于一个抽象方法,只能使用匿名内部类,而不能使用Lambda表达式
-
匿名内部类:编译之后,产生一个单独的.class字节码文件
Lambda表达式:编译之后,不会产生一个单独的.class字节码文件,对应的字节码会在运行的时候动态生成
异常类
Java代码在运行时期发生的问题就是异常
在Java中,把异常信息封装成了一个类,当出现了问题时,就会创建异常类对象并抛出异常相关的信息(如异常出现的位置、原因等)
注意:语法错误常称为编译时错误,语法错误不属于异常(异常的产生是建立在语法正确的基础上的)
异常的分类:
-
编译时期异常:在编译时期就会检查,如果异常没有得到处理,则编译失败,所以这类异常必须进行处理(或向上抛出让调用者处理)
非RuntimeException体系的异常称为编译异常
常见的编译异常有:IOException(输入输出异常)、SQLException(SQL异常)、ClassNotFoundException(类找不到异常)
-
运行时期异常:在运行时期检查异常,在编译时期,运行异常不会编译检查,这类异常可以不处理(也最好不要处理)
凡是RuntimeException和它的所有子类,都称为运行异常
常见的运行异常有:NullPointerException(空指针异常)、IndexOutOfBoundsException(索引越界异常)、ArithmeticException(算术异常)、ClassCastException(类型转换异常)
注意:多catch并行处理异常时,要求多个catch捕获的异常不能相同,并且若多个catch中的异常之间有子父类异常的关系,那么子类异常要求在上面的catch处理,父类异常在下面的catch处理
throw和throws 关键字
throw关键字:只能在方法内部使用,关键字的后面跟随异常对象的创建
throws关键字:只能使用在方法的定义上,关键字后面跟随异常类名
自定义异常
步骤:
-
自定义异常,继承Exception或者RuntimeException
编译时异常继承Exception,运行时异常继承RuntimeException
-
自定义异常类中的构造方法,super调用父类构造方法,并传递异常信息
public class MyException extends Exception {
String msg;
public MyException(String msg) {
super(msg);
this.msg = msg;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public void printMsg() {
System.out.println(msg);
}
}
Chap8 —— 常见实用类
Object类的equals方法比较的是对象的内存地址
Object类的equals方法源码:
public boolean equals(Object obj){
return this == obj;
}
引用数据类型之间作“==”比较就是比较对象的内存地址是否相同
class Person {
private String name;
private int age;
public Person(String name, int age){
this.name = name;
this.age = age;
}
}
class Test {
public static void main(String[] args){
Person p1 = new Person("张三", 20);
Person p2 = new Person("张三", 20);
System.out.println(p1.equals(p2)); // false
}
}
String类
字符串类java.lang.String类,继承Object类,实现了三个接口
字符串一旦初始化就不可以被改变,即字符串对象是常量,一旦创建不能修改(这里的不可以改变指的是字符串常量不可改变,但允许把一个全新的字符串常量赋值给字符串变量)
自动装箱与自动拆箱
自动装箱:自动装箱是指将基本数据类型自动转换成它们对应的包装类。例如,当你将一个 int 类型的值赋给一个 Integer 类型的变量时,Java 会自动将 int 值装箱成 Integer 对象
Integer i = 10; // 自动装箱,等同于 Integer i = Integer.valueOf(10);
自动拆箱:与自动装箱相反,自动拆箱是指将包装类对象自动转换成它们对应的基本数据类型。例如,当你将一个 Integer 类型的对象赋值给一个 int 类型的变量时,Java 会自动调用该对象的 intValue() 方法(对于 Integer 而言)来获取其内部的基本类型值,从而完成拆箱
Integer num = 10;
int numValue = num; // 自动拆箱,等同于 int numValue = num.intValue();
当包装类型与对应的基本数据类型进行“==”比较时会先进行自动拆箱处理,于是就变为了两个基本数据类型之间的值的相等比较
注意:包装类中的equals方法和String类一样,都是重写了Object类中的equals方法,因此比较的是内容而不是地址
Chap10 —— 输入输出流
IO流对象的分类
按照数据的流向分类:
- 输入流:Java程序从其它地方读取数据
- 输出流:Java程序中的数据,写入到其它地方
字节输出流:OutputStream,抽象类
字节输入流:InputStream,抽象类
字符输出流:Writer,抽象类
字符输入流:Reader,抽象类
FileOutputStream
构造方法 :
- FileOutputStream(File file)
- FileOutputStream(String path)
创建字节输出流对象,绑定参数就是要写入数据的目的
追加写入:FileOutputStream构造方法的第二个参数写true
换行写入:使用Windows系统的换行符号\r\n
注意:当文件路径不存在时会自动创建文件,且默认为覆盖式写入
FileInputStream
构造方法:
- FileInputStream(File file)
- FileInputStream(String path)
创建字节输入流对象,绑定参数就是要读取的数据源文件
BufferedInputStream、BufferedOutputStream
BufferedInputStream、BufferedOutputStream是修饰流,需要封装在已有的基础流之上
BufferedOuputStream的构造方法:BufferedOutputStream(OutputStream out),传递字节输出流
new BufferedOuputStream(new FileOutputStream());
BufferedInputStream的构造方法:BufferedInputStream(InputStream in),传递字节输入流
new BufferedInputStream(new FileInputStream())
FileWriter
FileWriter是字符输出流,写入文本文件,直接采用默认的编码表
构造方法:
- FileWriter(File file)
- FileWriter(String path)
FileReader
FileReader是字符输入流,读取文本文件,直接采用默认的编码表
构造方法:
- FileReader(File file)
- FileReader(String path)
BufferedReader、BufferedWriter
BufferedReader、BufferedWriter是修饰流,需要封装在已有的基础流之上
BufferedWriter的构造方法:BufferedWriter(Writer w),传递字符输出流
new BufferedWriter(new FileWriter());
BufferedWriter独有的方法:newLine(),写入文本换行符,平台无关
BufferedReader的构造方法:BufferedReader(Reader r),传递字符输入流
new BufferedReader(new FileReader());
BufferedReader独有的方法:String readLine(),读取文本一行,平台无关
对象的序列化与反序列化
ObjectOutputStream,对象的序列化
- 构造方法:ObjectOutputStream(OutputStream out),传递字节输出流
方法:void writeObject(Object o),写入对象
注意:被序列化的对象需要实现Serializable接口,Serializable接口是一个标记接口,其中不包含任何抽象方法
ObjectInputStream,对象的反序列化
- 构造方法:ObjectInputStream(InputStream out),传递字节输入流
方法:Object readObject()读取对象
class Fruit implements Serializable {
private static final long serialVersionUID = -7861870608146061984L;
private String fruitName;
private double fruitPrice;
@Override
public String toString() {
return "Fruit{" +
"fruitName='" + fruitName + '\'' +
", fruitPrice=" + fruitPrice +
'}';
}
// getter and setter...
public Fruit(String fruitName, double fruitPrice) {
this.fruitName = fruitName;
this.fruitPrice = fruitPrice;
}
}
class Test {
public static void main(String[] args) throws Exception {
Fruit fruit1 = new Fruit("柠檬", 15.0);
Fruit fruit2 = new Fruit("香蕉", 10.0);
Fruit fruit3 = new Fruit("苹果", 5.0);
Fruit fruit4 = new Fruit("草莓", 15.0);
List<Fruit> list1 = new ArrayList<>();
list1.add(fruit1);
list1.add(fruit2);
list1.add(fruit3);
list1.add(fruit4);
FileOutputStream fos = new FileOutputStream("D:\\桌面\\test.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(list1);
oos.close();
FileInputStream fis = new FileInputStream("D:\\桌面\\test.txt");
ObjectInputStream ois = new ObjectInputStream(fis);
List<Fruit> list2 = (List<Fruit>)ois.readObject();
ois.close();
for (Fruit fruit : list2) {
System.out.println(fruit);
}
}
}
IO流的关闭
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class FileExample {
public static void main(String[] args) {
BufferedReader reader = null;
BufferedWriter writer = null;
try {
// 打开文件以读取
reader = new BufferedReader(new FileReader("input.txt"));
// 打开文件以写入
writer = new BufferedWriter(new FileWriter("output.txt"));
// 这里可以添加读取和写入文件的代码
} catch (IOException e) {
e.printStackTrace();
// 处理异常
} finally {
// 在finally块中关闭文件流
try {
if (reader != null) {
reader.close();
}
if (writer != null) {
writer.close();
}
} catch (IOException e) {
e.printStackTrace();
// 如果关闭时发生异常,也需要处理
}
}
}
}
Chap15 —— 泛型与集合框架
集合与数组的对比
共同点: 集合,数组都是容器,都可以存储数据
区别:
- 集合只能存储引用类型数据,不能存储基本类型数据
- 数组可以存储基本类型,也可以存储引用类型
- 数组定长,集合容器变长
注意:所有集合类都位与java.util包中
Collection接口
Collection是所有单列集合的顶级接口,任何单列集合都是它的子接口或实现类,该接口中定义的方法是所有单列结合的共性方法
Collection接口中的常用方法:
| 方法的定义 | 方法作用 |
|---|---|
| boolean add(E) | 添加元素到集合中 |
| void clear() | 清空集合容器中的元素 |
| boolean contains(E) | 判断元素是否在集合中 |
| boolean isEmpty() | 判断集合的长度是否为0,是0则返回true |
| int size() | 返回集合的长度,也就是集合中元素的个数 |
| boolean remove(E) | 移除集合中指定的元素,移除成功后返回true |
Iterator迭代器接口
Iterator接口的抽象方法:
- boolean hasNext():判断集合中是否有下一个可以遍历的元素,如果有则返回true
- E next():获取集合中下一个元素
- void remove():移除遍历到的元素
并发修改异常
并发修改异常产生的原因:迭代器遍历集合的过程中,使用了集合的功能,改变了集合的长度所造成
List接口
List接口继承Collection接口,是单列集合
List接口的特点:
-
允许元素重复
-
元素有序
这里的有序是指元素取出时的顺序和存储时的顺序时一致的
List接口特有的方法:
- add(int index, E e):指定的索引位置,添加元素
- get(int index):返回指定索引上的元素
- set(int index, E e):修改指定索引上的元素,返回被修改之前的元素
- remove(int index):移除指定索引上的元素,返回被移除之前的元素
List接口中的方法listIterator():返回迭代器,迭代器的接口是ListIterator,是List集合的专用迭代器
ListIterator迭代器接口中的抽象方法:
- boolean hasNext()
- E next()
- boolean hasPrevious():判断集合中是否有上一个元素,反向遍历
- E previous():取出集合的上一个元素
ArrayList集合
ArrayList类实现List接口,具备List接口的特性(有序、重复、索引)
ArrayList集合底层的实现原理是数组,但大小可变(存储对象的时候长度无需考虑)
特点:查询速度快,增删慢
初始长度是10个,每次扩容是原来长度的1.5倍
ArrayList是线程不安全的集合,运行速度快
LinkedList集合
LinkedList类实现接口List接口,具备List接口的特性(有序、重复、索引)
LinkedList底层实现原理是链表,双向链表
特点:增删速度快,查询慢
LinkedList是线程不安全的集合,运行速度快。
Set集合
Set集合,是接口Set,继承Collection
Set集合不存储重复元素,重复元素直接忽视,Set集合下的所有实现类,都会具有这个特性
Set接口的方法和父接口Collection的方法完全一样
HashSet集合
HashSet集合实现Set接口
HashSet底层实现原理是哈希表
HashSet不保证迭代顺序,即元素存储和取出的顺序不一定相同
线程不安全,运行速度快
HashSet集合元素的唯一性是根据对象的hashCode和equals方法来决定的
在往HashSet集合中存放自定义的对象时,为了保证唯一,就必须重写对象的hashCode和equals方法建立属于当前对象的比较方式
哈希值相关的问题:两个对象的hashCode相同,则equals不一定返回true;两个对象的equals返回true,两个对象的hashCode必须一致
class Student {
private String id;
private String name;
private double score;
public Student(String id, String name, double score) {
this.id = id;
this.name = name;
this.score = score;
}
// getter and setter...
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return Double.compare(student.score, score) == 0 &&
Objects.equals(id, student.id) &&
Objects.equals(name, student.name);
}
@Override
public int hashCode() {
return Objects.hash(id, name, score);
}
@Override
public String toString() {
return "Student{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", score=" + score +
'}';
}
}
class Test {
public static void main(String[] args) {
HashSet<Student> hashSet = new HashSet<>();
hashSet.add(new Student("001", "张三", 90));
hashSet.add(new Student("002", "李四", 100));
hashSet.add(new Student("003", "王五", 93));
hashSet.add(new Student("002", "李四", 100));
Iterator<Student> iterator = hashSet.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
}
}
TreeSet集合
TreeSet集合实现Set接口
TreeSet底层是红黑树,依赖于TreeMap的实现
特点:查找速度快,线程不安全,元素有序(这里的有序是按字典顺序)
TreeSet集合在存储自定义对象时,自定义对象需要实现Comparable接口,并重写接口中的compareTo()方法
class Student implements Comparable<Student> {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public int compareTo(Student o) {
return this.age - o.age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
class Test {
public static void main(String[] args) {
TreeSet<Student> students = new TreeSet<>();
students.add(new Student("张三", 21));
students.add(new Student("李四", 19));
students.add(new Student("王五", 20));
Iterator<Student> iterator = students.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
}
}
除了让自定义对象实现Comparable接口的方式外,还可以在创建TreeSet集合时往构造方法中添加Comparator接口的实现类对象,实现类对象中重写Comparator接口中的compare()方法
public class Person {
private String name;
private String id;
public Person(String name, String id) {
this.name = name;
this.id = id;
}
// getter and setter...
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", id='" + id + '\'' +
'}';
}
}
class Test {
public static void main(String[] args) {
TreeSet<Person> persons = new TreeSet<>(new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
return o1.getId().compareTo(o2.getId());
}
});
persons.add(new Person("张三", "114920230000"));
persons.add(new Person("李四", "114920230003"));
persons.add(new Person("王五", "114920230001"));
persons.add(new Person("赵六", "114920230002"));
Iterator<Person> iterator = persons.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
}
}
注意:TreeSet内元素的唯一性是根据要排序的属性决定的,并且不会发生覆盖的现象
LinkedHashSet集合
LinkedHashSet集合的底层数据结构是哈希表,继承HashSet
LinkedHashSet数据是双向链表,有序的集合,存储和取出的顺序一样
泛型Generic
语法:集合类<存储的数据类型> 变量名 = new 集合类<存储的数据类型>();
泛型限定:
- :传递E类型或者是E的子类,泛型上限限定
- 传递E类型或者是E的父类,泛型下限限定
Map集合
Map集合,是Map接口,是双列集合的顶级接口
Map集合容器每次存储2个对象,一个对象称为键(Key),一个对象称为值(Value)
在一个Map的集合中,键保证唯一性,不包含重复键,每个键只能对应一个值
键重复时会出现覆盖现象,所有Map接口的实现类都具有这个特点
Map接口方法:
- V put(K, V):存储键值对,存储重复键时,返回被覆盖之前的值
- V get(K):通过键获取值,参数传递键,找这个键对应的值,没有这个键时返回null
- boolean containsKey(K):判断集合是否包含这个键,包含返回true
- boolean containsValue(V):判断集合是否包含这个值,包含返回true
- int size():返回集合长度,即Map集合中键值对的个数
- V remove(K):移除指定的键值对,返回被移除之前的值
- Collection<V> values():Map集合中的所有的值拿出,存储到Collection集合
Map集合的遍历:
- Map接口定义了方法Set<K> keySet(),用该方法获取Map中所有的键,并存储到Set集合中
- 遍历此Set集合,取出Set集合的元素, Set集合的元素是Map集合的键
- 使用Map集合方法get(),往其中传递键以获取值
除了上面的方法外,还可以使用entrySet()获取每一个键值对对象
Set<Map.Entry<Key, Value>> entrySet():方法返回Set集合,集合中的元素存储的是Map集合中键值对映射关系的对象。之后遍历Set集合的元素,并使用getKey()和getValue()来获取键和值
HashMap集合
HashMap集合特点:
- 是哈希表结构
- 保证键唯一性,作为键的对象,必须重写hashCode和equals方法(会发生覆盖)
- 线程不安全集合,运行速度快
- 允许使用null作为键或者值
class Person {
private String name;
private int age;
// getter and setter...
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age &&
Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
class Test {
public static void main(String[] args) {
HashMap<Person, String> hashMap = new HashMap<>();
hashMap.put(new Person("张三", 20), "001");
hashMap.put(new Person("李四", 21), "002");
hashMap.put(new Person("王五", 21), "003");
hashMap.put(new Person("赵六", 22), "004");
hashMap.put(new Person("刘七", 20), "005");
hashMap.put(new Person("李四", 21), "007");
Set<Person> keySet = hashMap.keySet();
for (Person key : keySet) {
System.out.println("key:"+key+" value:"+hashMap.get(key));
}
}
}
LinkedHashMap集合
LinkedHashMap继承HashMap,实现Map接口,LinkedHashMap底层实现原理是哈希表,双向链,存取有序,其它的特性和父类HashMap一样
TreeMap集合
TreeMap集合的特点:
- 底层实现是红黑树结构(添加查询速度比较快)
- 存储到TreeMap中元素,按照键进行排序,是对象的字典顺序排序(会出现覆盖现象)
- 作为键的对象,需要实现接口Comparable;或自己提供比较器,实现接口Comparator,优先级高
- 线程不安全,运行速度快
public class Student {
private String name;
private int age;
// getter and setter...
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
class Test {
public static void main(String[] args) {
Map<Student, String> treeMap = new TreeMap<>(new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
return o1.getAge()-o2.getAge();
}
});
treeMap.put(new Student("张三", 20), "张三");
treeMap.put(new Student("李四", 19), "李四");
treeMap.put(new Student("王五", 22), "王五");
treeMap.put(new Student("王五", 22), "王五1");
Set<Map.Entry<Student, String>> entrySet = treeMap.entrySet();
Iterator<Map.Entry<Student, String>> iterator = entrySet.iterator();
while(iterator.hasNext()){
Map.Entry<Student, String> item = iterator.next();
System.out.println("key=" + item.getKey() + " value=" + item.getValue());
}
}
}