oo
面向对象
一切皆对象,面向对象语言用“类”来抽象化“对象”。类可以实例化出任意数量的对象。
面向对象编程在大型项目、复杂系统开发中具有明显优势,尤其是在需要处理多个具有相似特征的实体时。
复杂性控制机制
封装
黑匣子,控制内部复杂性使得外部不可见。
隐藏类实现细节的机制,降低用户代码和类实现之间的耦合度。
继承
通过建立类之间的抽象层次来协同降低复杂性。
子类获得父类的设计与实现–》复用和扩展,也可改写
利用继承的方法,可以重用已存在类的方法和属性,而不用重写这些代码。被继承的类称为超类(super class),派生类称为子类(sub class)。
extends
定义继承关系:
public class SubClass extends SuperClass(…)
子类可以访问父类的公共和受保护成员
私有成员无法直接被访问,需调用父类的方法来访问
Java只支持单继承,即一个类只能有一个父类
implements
定义继承关系
使java具有多继承得特性,使用范围为类继承接口的情况,可同时继承多个接口(接口间采用逗号分隔)
1 |
|
通过此种方式可以实现多继承即一个子类继承多个父“类”(接口)
父类概括子类
使用obj instanceof MyClass
表达式来判断obj的类型是否为Myclass
在子类中,使用super.attribute可以引用父类中定义的非私有属性
super.methodName()可以调用父类所实现的方法
this.methodName()可以调用子类自己的方法
final
关键字可用来修饰变量(包括类属性、对象属性、局部变量和形参)、方法(包括类方法和对象方法)和类。
使用其声明类,即将类定义为最终类,不可被继承,或用于修饰方法,该方法不能被子类重写。
注意:final
定义的类中的不被final
修饰的属性和方法不是final
的。
子类构造方法中,一般使用super(arguments)来调用父类的构造方法,从而完成对父类所定义属性的初始化。子类拥有父类非private
的属性、方法。
当父子类中出现同名方法时,观察创建的对象类型,是哪类就优先调用那类的方法,若本类中不存在该方法,就向上寻找父类方法进行调用。
注意:
由于继承的存在,引用变量在定义时,左侧的变量类型一定要在父子类关系上一定要大于右侧的构造器所对应的变量类型,而通过什么构造器实例化的的对象就是什么对象,在左侧的变量类型大于右侧时,也不用管;示例:
1 |
|
其中a是实打实的Animal对象,只能调用Animal类中有的方法;而b和c都是Dog对象,但前者是Animal变量类型而后者是Dog变量类型,即b虽然是Dog对象,但是却不是”彻底的“Dog对象,因为继承关系,所以当b调用Dog类方法时,若Animal类中没有相应的方法,则会出现报错的情况,而c就没有这种情况,它可以调用Dog类和Animal类中的所有可调用的方法。
其实是java编译的时候做的是引用的检查,b的引用类型是Animal
而Animal
类中并没有bark()
方法,因此编译时会报错,但运行时调用的是实际对象的方法,所以无论是b还是c俩引用对应的对象都是Dog
类的对象,因此会找到Dog
类的对应方法。
而Java的实例方法调用是基于运行时的实际类型的动态调用,而非变量的声明类型,变量的声明类型决定了运行时的检查。
类的继承格式
1 |
|
为了提高代码的复用性,使代码更加简洁,采用继承的方式是很好的。例:
1 |
|
构造器
子类不继承父类的构造器,只调用(隐式或显式)。若父类的构造器(即初始化方法)中带有参数,则必须在子类的构造器中显式通过super
关键字调用父类的构造器并配以适当的参数列表;反之,子类的构造器中不需要使用super
关键字调用父类构造器,系统会自动调用父类的无参数构造器。
1 |
|
结果为:
1 |
|
即:在子类中不带参数和带参数的构造器中使用super(参数)会调用父类中的带参数的构造器,而不使用会隐式调用父类中不带参数的构造器。
继承中的转型
向上转型
在建立了继承关系之后,可以使用父类型去引用通过子类型创建的对象。这里涉及两个重要的概念,对象与对象引用。一般而言,对象是一个类的实例化结果,对应内存中的一个数据结构。对象引用则是使用一个变量来指向内存中的这个数据结构(即对象)。
如我们可以使用上面的 Dog 类来构造一个对象:new Dog()
,这条语句返回一个创建的对象。我们同时需要声明一个对象引用来指向返回的对象,否则可能就找不到这个对象了。所以,一般代码都会这么写:Dog bernese = new Dog()
。
在建立了继承关系之后,我们也可以使用 Animal 类来声明一个对象引用,并指向类型为 Dog 的对象:Animal pet = new Dog(...)
。从程序类型的角度,这个表达方式称为向上的类型转换,简称向上转型 (up cast)。
向下转型
Java 语言提供了一个特殊的关键词 instanceof
用来判断一个对象引用所指向的对象的创建类型是否为特定的某个类,一般写为 obj instanceof A
,其中 obj 为一个对象引用,A 为一个类型(类或接口),这个表达式的取值结果为布尔型,如果 obj 的创建类型为 A,则结果为 true,否则为 false。在这个表达式取值为 true 的情况下,可以使用向下转型 (down cast) 来使用一个 A 类型的对象来引用obj: A ao = (A)obj
。注意,实际上 obj 所指向对象的创建类型永远不会发生变化,转型的只是对象引用类型。下面例子给出了相应的向下转型场景:
1 |
|
Override/Overload
Override
注意:子类重写时方法名称、参数列表和返回类型须保持一致
重写规则
返回类型可与被重写方法的返回类型不同,但须是父类返回值的派生类(其本身或其子类)。
访问权限不能比父类中被重写的方法的访问权限更低。
声明为
static
的方法不能被重写,但能被再次声明。子类父类同在一个包中,则子类可重写父类所有方法,除了声明为
private
和final
的方法。不在同一个包中,则子类只能够重写父类的声明为
public
和protected
的非final
方法。构造方法不能被重写,即初始化方法不可被重写
不能重写父类不存在的方法(
显而易见?)@Override注解:用于标记重写方法,提高代码可读性
重写方法必须与父类方法相同,包括参数个数和参数列表顺序、参数类型。
子类可以根据需要实现父类的方法,如此一来,使用子类对象调用该方法时,将执行子类中的方法而非父类的方法。可以使用super
关键字来调用父类的方法。
需要在子类中调用父类的被重写方法时,需要使用super
关键字。
使用场景
提炼场景:
多个类之间存在相同的属性和方法
将相同的部分提取出来形成一个类
避免出现重复代码
更好维护相同的属性和代码
扩展场景:
引入新类,避免对已有类进行修改
新类对已有类进行增量式扩展
避免编写冗余代码
保持已有类及其使用者的稳定
Overload
在类中,方法名相同,但参数、返回类型可以不同。
重载规则
被重载的方法必须改变参数列表(参数个数或类型不同)
被重载的方法可以改变返回类型
被重载方法可以改变访问修饰符
被重载的方法可以声明新的或更广的检查异常
方法能够在同一个类中或一个子类中被重载
无法以返回值类型作为重载函数的区分标准
代码:
1 |
|
总结
方法的重写(Overriding)和重载(Overloading)是java多态性的不同表现,重写是父类与子类之间多态性的一种表现,重载可以理解成多态的具体表现形式。
(1)方法重载是一个类中定义了多个方法名相同,而他们的参数的数量不同或数量相同而类型和次序不同,则称为方法的重载(Overloading)。
(2)方法重写是在子类存在方法与父类的方法的名字相同,而且参数的个数与类型一样,返回值也一样的方法,就称为重写(Overriding)。
(3)方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现。
阻止继承
从java15开始,允许使用sealed
修饰class并通过permits
明确指出能够从该class继承的子类名称,例:
1 |
|
其中Shape
类就是一个sealed
类,只允许指定的3个类继承。为的是防止继承被滥用。
sealed
类在Java15中目前为预览状态,须使用参数--enable-preview
和--source 15
来启用。
多态
类通过多种形态方法来解耦复杂性。
针对某个类型的方法调用,其真正执行的方法取决于运行时期实际类型的方法
对于一个指令(方法名),一个类有多种功能形态。(前提:功能在宏观上有一致性。)
基于接口的多态实现
针对抽象编程你,而不是针对具体实现编程
虚函数
若Java中不希望某个函数具有虚函数特性,可加上final关键字变成非虚函数。
Java
类名称与相应程序文件的名称一致,java的执行以类为单位。java采用类及其协同,调用+数据管理+抽象层次
注释
三种注释方式
1.行注释
1 |
|
2.块注释
1 |
|
3.Javadoc注释(推荐)
1 |
|
文档注释的格式通常包含一些特定的标签,如 @param 用于描述方法参数,**@return** 用于描述返回值,**@throws** 用于描述可能抛出的异常等等,这些标签有助于生成清晰的API文档,以便其他开发者能够更好地理解和使用你的代码。
类型
超过8字节的超大整数:BigInteger(类)
boolean (true\false)注意是首字母小写,不是大写。
String不是基础类型,是类
char是一个单一的16位Unicode字符,占两个字节
构造类型:类(class)、接口(interface)、枚举(enum)
内置数据类型
八种基本类型(六种数字类型(四种整数型、两个浮点数型)、一种字符类型、一种布尔类型)
byte
8位、有符号的,以二进制补码表示的整数,byte类型用在大型数组中节约空间,主要代替整数,因为 byte 变量占用的空间只有 int 类型的四分之一,默认值为0
.
short
16位、有符号的,Short 数据类型也可以像 byte 那样节省空间。一个short变量是int型变量所占空间的二分之一;默认值为0.
int
同C,但默认值为0(未初始化的变量的值)
long
64位,有符号的,主要使用在需要比较大整数的系统上,默认值0L
float和double均与C相同
但是前者默认值0.0f
后者默认值0.0d
boolean
默认值false
char
单一的16位Unicode字符,可以储存任何字符,默认值为’u0000’
*void
对应得包装类为java.lang.Void
,但无法对其直接操作。
String(or any object)
默认值null
引用类型
在Java中,引用类型的变量非常类似于C/C++的指针。引用类型指向一个对象,指向对象的变量是引用变量。这些变量在声明时被指定为一个特定的类型,比如 Employee、Puppy 等。变量一旦声明后,类型就不能被改变了。
对象、数组都是引用数据类型。
所有引用类型的默认值都是null。
一个引用变量可以用来引用任何与之兼容的类型。
例子:Site site = new Site(“Runoob”)。
Java常量
常量运行时不可修改。
用final
修饰常量
1 |
|
通常用大写字母表示常量。
字符串常量是包含在两个引号之间的字符序列,字符串常量和字符变量都可以包含任何Unicoe字符,
1 |
|
一些特殊的转义字符序列:\b
:退格;\s
:空格;\ddd
:八进制字符;\uxxxx
:16进制Unicode字符(xxxx)
var关键字
可以用var
关键字来省略变量类型,例:
1 |
|
谨慎尝试,对于不同的编辑器或者不同版本的jdk可能不兼容
运算小规则
比int类型小的类型做运算,java在编译的时候就会将它们统一强转成int类型。当是比int类型大的类型做运算,就会自动转换成它们中最大类型那个(自动转换类型)。
类型的传递方式
基础数据类型是值传递,而引用数据类型是引用传递。前者(int、boolean等)在作为参数传递时,传递的是真实的数据,只改变形参,不影响实参。而后者(类、数组、接口等)作为参数传递时,传递的是堆内存中的地址,即引用,形参和实参均发生改变。
变量作用域范围
类级
类中声明的静态成员变量(static int classInt)
一个类的所有实例化对象共享访问该类的静态成员变量
对象级
类中声明的普通成员变量
每个实例化对象拥有一份独立的变量
方法级
方法中声明的局部变量,包括参数中声明的变量和方法体中声明的变量
每次方法执行时变量取值都会进行初始化,与上一次的运行==无关==
语句块级
方法语句块中声明的局部变量,能够覆盖所有上一层次语句块所声明的变量
变量类型
实例变量
在类中声明,但在方法、构造函数和块之外,属于类的实例,每个类的实例都有自己的副本,如果不明确初始化,实例变量会被赋予默认值(数值类型为0,boolean类型为false,对象引用类型为null)。
1 |
|
成员变量的值应该至少被一个方法、构造方法或者语句块引用,使得外部能够通过这些方式获取实例变量信息。
成员变量可以声明在使用前或者使用后。
访问修饰符可以修饰成员变量。
成员变量对于类中的方法、构造方法或者语句块是可见的。一般情况下应该把成员变量设为私有。通过使用访问修饰符可以使成员变量对子类可见。
成员变量具有默认值。数值型变量的默认值是0,布尔型变量的默认值是 false,引用类型变量的默认值是 null。变量的值可以在声明时指定,也可以在构造方法中指定;
成员变量可以直接通过变量名访问。但在静态方法以及其他类中,就应该使用完全限定名:ObjectReference.VariableName。
1
accessModifier type variableName
- accessModifier –表示访问修饰符,可以是 public、protected、private 或默认访问级别(即没有显式指定访问修饰符)。
- type – 表示变量的类型。
- variableName – 表示变量的名称。
与局部变量不同,成员变量的值在创建对象时被分配,即使未对其初始化,它们也会被赋予默认值,例如 int 类型的变量默认值为 0,boolean 类型的变量默认值为 false。
成员变量可以通过对象访问,也可以通过类名访问(如果它们是静态成员变量)。如果没有显式初始化成员变量,则它们将被赋予默认值。可以在构造函数或其他方法中初始化成员变量,或者通过对象或类名访问它们并设置它们的值。
静态变量/类变量
类变量是在类中用 static
关键字声明的变量,它们属于类而不是实例,所有该类的实例共享同一个类变量的值,类变量在类加载时被初始化,而且只初始化一次。
1 |
|
可通过类名或实例名来访问。
静态变量的线程安全性
Java 中的静态变量是属于类的,而不是对象的实例。因此,当多个线程同时访问一个包含静态变量的类时,需要考虑其线程安全性。
静态变量在内存中只有一份拷贝,被所有实例共享。因此,如果一个线程修改了静态变量的值,那么其他线程在访问该静态变量时也会看到修改后的值。这可能会导致并发访问的问题,因为多个线程可能同时修改静态变量,导致不确定的结果或数据一致性问题。
为了确保静态变量的线程安全性,需要采取适当的同步措施,如同步机制、原子类或 volatile 关键字,以便在多线程环境中正确地读取和修改静态变量的值。
参数变量
参数是方法或构造函数声明中的变量,用于接收调用该方法或构造函数时传递的值,参数变量的作用域只限于方法内部或构造函数内部。
1 |
|
方法参数变量的值传递方式:值传递(只传副本,不影响原始值,即拷贝)、引用传递(类似C中指针的用法,传递的是对象地址)
修饰符
default
若在类、变量、方法或构造函数的定义中未指定任何访问修饰符,则它们默认有默认访问修饰符(default
),访问级别是包级别,即只能被同一包中的其他类访问。
protected
子类与基类在同一包中
被声明为protected
的变量、方法和构造器能被同一个包中的任何其他类访问。
子类与基类不在同一个包中
在子类中,子类实例可访问其从基类继承而来的protected
方法,但不能访问基类实例的protected
方法。这里的说法或许有些拗口,通过下面这里例子或许会加深对protected
修饰符的理解:
1 |
|
对1而言,clone()
方法源自MyObjec2t本身,由于protected
修饰符,该方法的可见性仅为包p2以及MyObject2的子类,Test2虽然是MyObject2的子类,但是通过MyObject2实例化出来的obj是不能访问方法clone()的,而对于2而言,obj2是由Test2类实例化出来的,是在Test2类中访问其本身实例的从基类MyObject2继承来的clone(),因此编译通过。
可修饰数据成员、构造方法、方法成员,不能修饰类(除内部类)、接口及其成员变量。
注意:方法继承规则:
- 父类中声明为 public 的方法在子类中也必须为 public。(父公子公开)
- 父类中声明为 protected 的方法在子类中要么声明为 protected,要么声明为 public,不能声明为 private。(父保子不私)
- 父类中声明为 private 的方法,不能够被子类继承。(父私子不拥)
static
static关键字声明的静态方法独立于对象,不能使用类的非静态变量。
final
- 修饰变量,则必须显式指定初始值,不能被重新赋值。(意味“最后”的值)
- 修饰的方法可被子类继承但不能被重写。(“最后”的方法)
- 修饰的类不能被任何类继承。
abstract
不能实例化对象,唯一目的为了将来对该类的扩充。
一个类不可同时被abstract
和final
修饰,若包含抽象方法,则类一定为抽象类。
抽象类可以包含抽象方法和非抽象方法。
抽象方法
无任何实现的方法,具体实现由子类提供。
不能被声明为final
和static
。
继承抽象类的子类必须实现父类的所有抽象方法,除非子类也为抽象类。
抽象类可不包含抽象方法。
抽象方法的声明以分号结尾。例:public abstract sample();
synchronized
修饰的方法同一时间只能被一个线程访问,可与四个访问修饰符搭配使用。
例:public synchronized void showDetails(){}
transient
序列化的对象包含被其修饰的实例变量时,JVM跳过该特定的变量。即:被transient修饰的变量不能在序列化后写入相应的文件,同样反序列化时,也不会被持久化和恢复。
使用在定义变量的语句中来预处理类和变量的数据类型。
volatile
修饰的成员变量每次被线程访问时,都强制从共享内存中重新读取值,且当成员变量发生变化时,会强制线程将变化值回写入共享内存。如此一来,两个不同线程总看到某成员变量的同一值。
使用场景之一,单例模式中采用DCL双锁检测机制,在多线程访问情况下,可使用volatile修饰,保证多线程下的可见性,但会影响部分性能,单线程下没必要。
例:
1 |
|
数组
数组变量本质上是一个对象指针,指向数组对象在内存中的地址。
可以通过arr.length()来访问arr的规模。
对于一维数组可以采用toString()
方法打印其内容:
1 |
|
对于二位数组同样可以使用deepToString()
方法打印其内容:
1 |
|
作为二位数组的数组,三维数组在内存中的结构为:
1 |
|
类
Java中的类由属性(定义数据结构)和方法(定义对数据结构的操作函数)组成
每个类都有一种构造方法(初始化属性变量,返回对象指针),用于实例化对象。
包含有数据结构以及对于数据结构的实现的一个类中可以用来定义属性。(类似于结构体变量的概念)
操作函数与对应的结构体一起封装在类中。
类名每个单词首字母大写,其它小写,如:TarenaStudent。
变量和方法:第一个单词小写,从第二个单词开始首字母大写,如:tarenaStudent。
常量:所有字母大写,每个单词之间用 _ 连接。
主类
java程序可以包含多个类,有一个类拥有main方法,public static void main(String[] args){…}
是程序的入口,入口方法常见流程:构建业务类(需要直接操作的)对象、获取输入、调取业务类对象处理获得的输入、进行相关操作及输出。其中主方法的String[] args
是一个命令行参数,由JVM接收用户输入并传给main
方法。例如:
1 |
|
可以利用接收到的命令行参数根据不同的参数执行不同的代码,而这类程序必须在命令行执行通过$ javac Main.java
编译运行。
main方法一般不做具体的业务处理,而是把输入请求交给业务类处理。
业务类按业务类别封装了业务数据和处理行为。
访问实例变量和方法
通过已创建的对象来访问成员变量和成员方法:
1 |
|
源文件声明规则
当在一个源文件中定义多个类,并且还有import语句和package语句时,要特别注意这些规则。
- 一个源文件中只能有一个 public 类
- 一个源文件可以有多个非 public 类
- 源文件的名称应该和 public 类的类名保持一致。例如:源文件中 public 类的类名是 Employee,那么源文件应该命名为Employee.java。
- 如果一个类定义在某个包中,那么 package 语句应该在源文件的首行。
- 如果源文件包含 import 语句,那么应该放在 package 语句和类定义之间。如果没有 package 语句,那么 import 语句应该在源文件中最前面。
- import 语句和 package 语句对源文件中定义的所有类都有效。在同一源文件中,不能给不同的类不同的包声明。
类有若干种访问级别,并且类也分不同的类型:抽象类和 final 类等。这些将在访问控制章节介绍。
接口
一种定义方法和常量的抽象类型,不提供方法实现,一个类通过继承接口的方式,从而来继承接口的抽象方法。
接口不是类,但编写接口的方式和类相似,类描述对象的属性和方法,接口则包含类要实现的方法。
只有方法声明,无属性。方法没有实现体,任何类都可以实现一个接口,实现接口中所定义的所有方法。类似于对象间相互通信的协议。
接口只定义派生要用到的方法,但是方法的具体实现完全取决于派生类。
以interface
来声明,一个类通过继承接口的方式,来继承接口的抽象方法,一个类通过继承接口的方式来继承接口的抽象方法。
除非实现接口的类是抽象类,否则该类要定义接口中的所有方法。
接口无法被实例化,但可以被实现,一个实现接口的类,必须实现接口内描述的所有方法,否则必须声明为抽象类。不过实现了接口的类可以实例化,且实例化对象可以通过接口来引用和访问。
抽象方法的声明不需要public
或private
等修饰,
接口与类的区别:
1.前者不能用于实例化对象。
2.前者无构造方法。
3.前者中所有方法必须是抽象方法,可使用default
关键字修饰的非 抽象方法。
4.接口不能包含成员变量,除了static
和final
变量。
5.接口并不是被类继承了,而是被类实现,因此类中要含有实现该接口中所有抽象方法的所有方法。
6.接口支持多继承。
接口特性:
接口中每一个方法也是隐式抽象的,接口中的方法会被隐式的指定为 public abstract(只能是 public abstract,其他修饰符都会报错)。
接口中可以含有变量,但是接口中的变量会被隐式的指定为 public static final 变量(并且只能是 public,用 private 修饰会报编译错误),实际上这些变量都是常量,即使没有显式声明这些修饰符。
接口中的方法是不能在接口中实现的,只能由实现接口的类来实现接口中的方法。
接口本身就是一个规格,一个模范,因此其属性也必须是一个标准化的常量。
接口的声明
1 |
|
例:
1 |
|
接口的实现
类实现接口时需要实现接口中的所有方法,否则,类须声明为抽象的类。
使用implements
关键字实现。
语法:
1 |
|
对应于接口的声明中的例子,例:
1 |
|
重写接口中声明的方法时,需注意:
类在实现接口的方法时,不能抛出强制性异常,只能在接口中,或者继承接口的抽象类中抛出该强制性异常。
类在重写方法时要保持一致的方法名,并且应该保持相同或者相兼容的返回值类型。
如果实现接口的类是抽象类,那么就没必要实现该接口的方法。
实现接口时需注意:
一个类可以同时实现多个接口。
一个类只能继承一个类,但是能实现多个接口。
一个接口能继承另一个接口,这和类之间的继承比较相似。
接口的继承
采用extends
关键字实现子接口继承父接口,例:
1 |
|
这样一来,实现Hockey接口的类需要实现6个方法,而Football的需要5个。
接口的多继承
接口允许多继承,例如:
1 |
|
标记接口
最常用的继承接口是没有包含任何方法的接口。
标记接口是没有任何方法和属性的接口.它仅仅表明它的类属于一个特定的类型,供其他代码来测试允许做一些事情。
标记接口作用:简单形象的说就是给某个对象打个标(盖个戳),使对象拥有某个或某些特权。
例如:java.awt.event 包中的 MouseListener 接口继承的 java.util.EventListener 接口定义如下:
1 |
|
没有任何方法的接口被称为标记接口。标记接口主要用于以下两种目的:
建立一个公共的父接口:
正如EventListener接口,这是由几十个其他接口扩展的Java API,你可以使用一个标记接口来建立一组接口的父接口。例如:当一个接口继承了EventListener接口,Java虚拟机(JVM)就知道该接口将要被用于一个事件的代理方案。
向一个类添加数据类型:
这种情况是标记接口最初的目的,实现标记接口的类不需要定义任何接口方法(因为标记接口根本就没有方法),但是该类通过多态性变成一个接口类型。
枚举
枚举限制变量只能是预先设定好的值。使用枚举可以减少代码中的 bug。
例:我们为果汁店设计一个程序,它将限制果汁为小杯、中杯、大杯。这就意味着它不允许顾客点除了这三种尺寸外的果汁。
1 |
|
注意:枚举可以单独声明或者声明在类里面。方法、变量、构造函数也可以在枚举中定义。
关系
类关系
1.继承关系
2.关联关系
类与接口的关系
1.实现关系
2.关联关系
接口间关系
继承关系
访问权限设置
public、
protected(任意本类对象或子类对象都能访问):将字段和方法的访问权限控制在继承树内部
private(只有本类对象才能访问)
static声明
static声明的属性与方法:要么是类级成员(通过类名加.访问),要么是对象级成员(通过对象名加.访问),静态属性可以被类的所有对象共享访问。
静态方法只能访问静态属性 vs 非静态方法能访问非静态属性和静态属性。(静者恒静,动者可静)
静态方法不能调用非静态方法 vs 非静态方法可以调用静态方法。
final
1.修饰类中的属性或变量。即”值”不能变。
这个值,对于基本类型来说,变量里面放的就是实实在在的值,如 1,”abc” 等。
而引用类型变量里面放的是个地址,所以用 final 修饰引用类型变量指的是它里面的地址不能变,并不是说这个地址所指向的对象或数组的内容不可以变,这个一定要注意。
例如:类中有一个属性是 final Person p=new Person(“name”); 那么你不能对 p 进行重新赋值,但是可以改变 p 里面属性的值 p.setName(‘newName’);
final 修饰属性,声明变量时可以不赋值,而且一旦赋值就不能被修改了。对 final 属性可以在三个地方赋值:声明时、初始化块中、构造方法中,总之一定要赋值。
2、final修饰类中的方法
作用:可以被继承,但继承后不能被重写。
3、final修饰类
作用:类不可以被继承。
this
指当前对象。在非静态方法中用this关键字可以用来指向当前代码运行时所处于的对象,使用方式类似python中的self参数。
Object
默认类,内置equals方法(判断输入的Object对象与this是否相同)、clone方法(对this克隆产生一个新对象)、toString方法(将this对象的内容转化为String)。
修饰符
方法
就是一个类所拥有的行为,一般格式为:
修饰符 返回值类型 方法名(形参列表){}
查询方法
用来查询一个对象所管理的私有化属性数据(getter)
也可以返回对象属性数据适当变换或计算后的结果
toString方法
不能修改对象的属性值。只要涉及业务关系的属性可以赋予getter方法。
修改方法
一定要清楚修改方法成功执行的前提条件和执行效果。
修改方法对于一个类实现其业务目标而言具有决定性作用。
除非有充分的条件,不建议有set方法。
重名的方法
提供多个构造方法是经常出现的
应用场景,用来做不同的对象的形式相同的初
始化。
注意:初始化的完整性!
重载(overload):
多个方法名重名,具有相近的功能。
一个方法名包含多个相近但有差异的功能
Java允许在一个类中定义多个重名的方法,前提是:参数列表要有所不同,包括顺序、个数、类型不同。
创建对象
设计好类之后便可以创建对象了,要选择合适的构造方法,取决于掌握了多少可用来初始化对象的数据。
对象引用
通过对象引用可以访问所引用对象的属性和方法。要确保对象引用的类型于所指向实际对象的类型一致,或者左侧类为右侧类的父类。
改变对象引用的取值,并不会改变被引用对象的值,即改变指针的值,并不会改变原有地址上的值。对象引用就是指针,是一个标识符,值为地址。
值与引用
Java中所有使用复合类型所声明的变量都是引用:泛指class、enum、interface,也包括用户自定义类、Java类库定义的类。
Java使用任意类型声明的数组变量也为引用,例:int[] arr;
参数调用时:基础类型直接传递值(拷贝)、引用类型传递引用本身(可理解为对象地址)
引用对象作为属性
定义类的属性时,难以只使用基础属性:使用一个类来声明对象引用(类似结构体成员的概念)(作为属性成员);在构造方法中调用相应类型的构造方法来设置该属性成员,这时这两个类之间就产生了关联关系。
switch表达式
->
从java12开始,switch
语句升级为更简洁的表达式语法,使用类似模式匹配(->)的方法,保证只有一种路径会被执行,且不需要break
语句:
1 |
|
对于->
,若有多条语句,需要用{}
括起来,不用写break
,->
只会执行匹配的语句,无穿透效应。
如此一来通过switch语句对某个变量赋值,可以如下书写:
1 |
|
yield
大多情况下,switch
内部会返回简单的值,但(前提是使用->
赋值时)若语句复杂,可以将其放入{…}
中,再用yield
返回一个值作为switch
语句的返回值:
1 |
|
对象容器
Java提供的一整套解决方案来管理数量不定且动态变化的一组对象, 提供了灵活的遍历和插入方法。最常使用的容器是ArrayList
,相当于不定长数组,可以根据需要在运行期自动扩展长度。
ArrayList
ArrayList
提供插入、检索、克隆和删除操作(add, get, clone, remove)。ArrayList
存储对象引用,而不是对象值(即存储对象地址,而不只是值,这样可以是容器所需要的存储空间小许多)
ArrayList
一种新的for语句,遍历容器中的对象(增强for)
1 |
|
ArrayList
本质上是一个有弹性的动态数组,容器中的元素实际上是对象(不是int,char等这样的基本类型),一些常见基本类型可以使用对应的包装类:
基本类型 | 引用类型 |
---|---|
boolean | Boolean |
int | Integer |
char | Character |
float | Float |
double | Double |
ArrayList
类是一个可以动态修改的数组,与普通数组不同的是其无固定大小的限制,可以添加或删除元素。
示例代码:
1 |
|
LinkedList
双向链表,同ArrayList相比,增加和删除操作效率更高,查找和修改效率低。
在java.util包中,除了可以通过普通创建方法,还可以使用集合创建方法:LinkedList<E> list = new LinkedList(Collection<? extends E> c);
HashMap
散列表,键值对映射。内部关系无序,难以按照对象间关系如大小或先后顺序进行访问。
HashSet
基于HashMap实现,一个不允许有重复元素的集合,但允许有null值得出现,不会记录插入的顺序,并不是线程安全的。
基于hash值得对象存储和管理
比较hash值,不同则添加;相同则进一步比较对象引用地址或equals结果。
Queue
FIFO,first in first out顺序操作元素
Deque
TreeMap
基于红黑树实现的有序键值对存储结构,支持按键的顺序遍历
TreeSet
有序集合,基于红黑树实现,提供自动排序功能,适用于须按顺序存储元素的场景。
PriorityQueue
如何合理选择容器
一门课选课学生,学号不同,学生属性包含学号、姓名、成绩等
则场景1:学生加入小班属性,支持按小班号查询,需要采用HashMap<cid,ArrayList<student>>
,一个小班号cid
对应一个小班
场景2:支持安排小班号+学号的综合排序:TreeMap<cid,TreeMap<sid,student>>
场景3:支持动态选择按照(小班号+学号)或(小班号+成绩)进行综合排序:TreeMap<cid,TreeMap<score,HashSet<student>>>
如何使用迭代器来遍历容器
1 |
|
Number & Math 类
Boolean、Byte、Short、Integer、Long、Character、Float、Double是相应基本类型的包装类,结构如下:
Math类可以在主函数中直接被调用,例如:Math.sin()、Math.cos()、Math.tan()、Math.PI、Math.atan
一些常用方法如下(参考自[菜鸟教程](Java Number & Math 类 | 菜鸟教程)):
序号 | 方法与描述 |
---|---|
1 | xxxValue() 将 Number 对象转换为xxx数据类型的值并返回。 |
2 | compareTo() 将number对象与参数比较。 |
3 | equals() 判断number对象是否与参数相等。 |
4 | valueOf() 返回一个 Number 对象指定的内置数据类型 |
5 | toString() 以字符串形式返回值。 |
6 | parseInt() 将字符串解析为int类型。 |
7 | abs() 返回参数的绝对值。 |
8 | ceil() 返回大于等于( >= )给定参数的的最小整数,类型为双精度浮点型。 |
9 | floor() 返回小于等于(<=)给定参数的最大整数 。 |
10 | rint() 返回与参数最接近的整数。返回类型为double。 |
11 | round() 它表示四舍五入,**算法为Math.floor(x+0.5)**,即将原来的数字加上 0.5 后再向下取整,所以,Math.round(11.5) 的结果为12,Math.round(-11.5) 的结果为-11。 |
12 | min() 返回两个参数中的最小值。 |
13 | max() 返回两个参数中的最大值。 |
14 | exp() 返回自然数底数e的参数次方。 |
15 | log() 返回参数的自然数底数的对数值。 |
16 | pow() 返回第一个参数的第二个参数次方。 |
17 | sqrt() 求参数的算术平方根。 |
18 | sin() 求指定double类型参数的正弦值。 |
19 | cos() 求指定double类型参数的余弦值。 |
20 | tan() 求指定double类型参数的正切值。 |
21 | asin() 求指定double类型参数的反正弦值。 |
22 | acos() 求指定double类型参数的反余弦值。 |
23 | atan() 求指定double类型参数的反正切值。 |
24 | atan2() 将笛卡尔坐标转换为极坐标,并返回极坐标的角度值。 |
25 | toDegrees() 将参数转化为角度。 |
26 | toRadians() 将角度转换为弧度。 |
27 | random() 返回一个随机数。 |
注意:1.Java会对-128 ~ 127之间的整数进行缓存,即:定义两个变量初始化值位于-128 ~ 127之间时,两变量使用同一地址,超出范围将使用不同的地址。
Character类
提供大量方法来操纵字符,声明定义方式如右:Character ch = new Character('a');
不过Java9之后由于静态工厂Character.valueOf(char)有更好的空间和时间性能。所以常使用:Character ch = new Character.valueOf('a');
常用Character方法
序号 | 方法与描述 |
---|---|
1 | isLetter() 是否是一个字母 |
2 | isDigit() 是否是一个数字字符 |
3 | isWhitespace() 是否是一个空白字符 |
4 | isUpperCase() 是否是大写字母 |
5 | isLowerCase() 是否是小写字母 |
6 | toUpperCase() 指定字母的大写形式 |
7 | toLowerCase() 指定字母的小写形式 |
8 | toString() 返回字符的字符串形式,字符串的长度仅为1 |
String类
1 |
|
注意:String类不可改变,一旦创建,值就无法改变,需要改变应选择StringBuffer & StringBuilder类
连接字符串
使用concat()
方法,如:string1.concat(string2);
创建格式化字符串
print()
和format()
方法,后者返回的是一个String对象而不是PrintStream对象,因此可用来创建可复用的格式化字符串而不仅是用于一次打印输出。
例:
1 |
|
题外话:整型的格式化(即String.format(“xxx”, num)中的xxx)
%[index$] [标识] [最小宽度] 转换方式
index从1(没有默认1)开始取,代表将第index个参数进行格式化,常见的标识有:
-
,左对齐,不可与零填充同用;
#
只用于8进制和16进制,前者在数前加上0,后者加上0x;
+
为正数加上一个’+’号,一般只适用十进制,对象为BigInteger才可用于8、16进制。
‘ ‘正数前加空格,负数前加负号,一般只适用十进制,对象为BigInteger才可用于8、16进制。
0
,0填充。
,
每3位数字间用,
分割
(
参数为负数,则不添加负号而用圆括号括起来,同+
有相同的限制。
转换方式:d(10)、o(8)、x/X(16)
浮点数的格式化就是在整型基础上加上精度再该改变转换方式。
转变方式为:
E
、e
:格式化为计算机科学计数法表示的十进制数
f
:格式化为十进制普通表示方式
G
、g
:自动选择普通表示还是科学计数法方式
A
、a
:格式化为带有有效位数和指数的十六进制浮点数
其中标识中的,
只适用于fgG,(
只适用于eEfGg。
常用String方法
选择String
、StringBuffer
和StringBuilder
的基本原则:
- 要操作少量数据用
String
- 单线程操作大量数据用
StringBuilder
- 多线程操作大量数据用
StringBuffer
多行字符串
从java13开始,字符串可以用"""…"""
表示多行字符串(Text Blocks)了。例:
1 |
|
StringBuffer和StringBuilder类
buffer较builder来说具有线程安全的特性,但builder具有速度优势。
常用的主要方法:
序号 | 方法描述 |
---|---|
1 | public StringBuffer append(String s) 将指定的字符串追加到此字符序列。 |
2 | public StringBuffer reverse() 将此字符序列用其反转形式取代。 |
3 | public delete(int start, int end) 移除此序列的子字符串中的字符。 |
4 | public insert(int offset, int i) 将 int 参数的字符串表示形式插入此序列中。 |
5 | insert(int offset, String str) 将 str 参数的字符串插入此序列中。 |
6 | replace(int start, int end, String str) 使用给定 String 中的字符替换此序列的子字符串中的字符。 |
Array类
通过fill方法给数组赋值、sort排序、equals比较数组中元素是否相等、binarySearch方法对排好序的数组进行二分查找:
序号 | 方法和说明 |
---|---|
1 | public static int binarySearch(Object[] a, Object key) 用二分查找算法在给定数组中搜索给定值的对象(Byte,Int,double等)。数组在调用前必须排序好的。如果查找值包含在数组中,则返回搜索键的索引;否则返回 (-(插入点) - 1)。 |
2 | public static boolean equals(long[] a, long[] a2) 如果两个指定的 long 型数组彼此相等,则返回 true。如果两个数组包含相同数量的元素,并且两个数组中的所有相应元素对都是相等的,则认为这两个数组是相等的。换句话说,如果两个数组以相同顺序包含相同的元素,则两个数组是相等的。同样的方法适用于所有的其他基本数据类型(Byte,short,Int等)。 |
3 | public static void fill(int[] a, int val) 将指定的 int 值分配给指定 int 型数组指定范围中的每个元素。同样的方法适用于所有的其他基本数据类型(Byte,short,Int等)。 |
4 | public static void sort(Object[] a) 对指定对象数组根据其元素的自然顺序进行升序排列。同样的方法适用于所有的其他基本数据类型(Byte,short,Int等)。 |
日期时间
Date类提供两个构造函数(Date(),参数是当前日期和时间;Date(long millisec)一个参数从1970年1月1日起的毫秒数)实例化Date对象。
序号 | 方法和描述 |
---|---|
1 | boolean after(Date date) 若当调用此方法的Date对象在指定日期之后返回true,否则返回false。 |
2 | Object clone( ) 返回此对象的副本。 |
3 | int compareTo(Date date) 比较当调用此方法的Date对象和指定日期。两者相等时候返回0。调用对象在指定日期之前则返回负数。调用对象在指定日期之后则返回正数。 |
4 | int compareTo(Object obj) 若obj是Date类型则操作等同于compareTo(Date) 。否则它抛出ClassCastException。 |
5 | boolean equals(Object date) 当调用此方法的Date对象和指定日期相等时候返回true,否则返回false。 |
6 | long getTime( ) 返回自 1970 年 1 月 1 日 00:00:00 GMT 以来此 Date 对象表示的毫秒数。 |
7 | int hashCode( ) 返回此对象的哈希码值。 |
8 | void setTime(long time) 用自1970年1月1日00:00:00 GMT以后time毫秒数设置时间和日期。 |
9 | String toString( ) 把此 Date 对象转换为以下形式的 String: dow mon dd hh:mm:ss zzz yyyy 其中: dow 是一周中的某一天 (Sun, Mon, Tue, Wed, Thu, Fri, Sat)。 |
使用SimpleDateFormat格式化日期
例如:
1 |
|
使用printf格式化日期(注意是printf而不是println)
- %tY:输出四位数的年份,例如:2023
- %ty:输出两位数的年份,例如:23
- %tm:输出两位数的月份,例如:02
- %tB:输出月份的全名,例如:February
- %tb:输出月份的缩写,例如:Feb
- %tA:输出星期的全名,例如:Wednesday
- %ta:输出星期的缩写,例如:Wed
- %td:输出两位数的日期,例如:24
- %te:输出一位或两位数的日期,例如:24 或 02
- %tH:输出24小时制的小时数,例如:23
- %tI:输出12小时制的小时数,例如:11
- %tM:输出分钟数,例如:45
- %tS:输出秒数,例如:30
- %tp:输出上午还是下午,例如:AM 或 PM
- %tZ:输出时区,例如:GMT+08:00
- %tc:按星期、月份、日期、时间、时区、年份输出全部日期和时间信息
- %tF:”年-月-日”格式输出
- %tD:”月/日/年”格式
- %tr:”HH:MM:SS PM”格式(12时制)
- %tT:”HH:MM:SS”格式(24)
- %tR:”HH:MM”格式(24)
可以使用**$
**来重复提供日期,例如: System.out.printf("%1$s %2$tB %2$td, %2$tY", "Due date:", date);
;或者使用<
标志表明先前被格式化的参数要被再次使用。例如:System.out.printf("%s %tb %<td, %<tY", "Due date:", date);
java休眠(sleep)
sleep()使当前线程进入停滞状态,即阻塞当前线程。
例:
1 |
|
Calendar类
抽象类,实际使用时实现特定的子类的对象。
创建一个代表系统当前日期的Calendar对象:Calendar c = Calendar.getInstance();//默认当前日期
,这样的设置是以开始作为1月的,11作为12月,12会变为年的1月,也就是来年的0.
可以在上述声明后通过set()
方法创建指定日期的Calendar对象,如:~;c.set(2009, 5, 6);
若只设定某个字段,则可以使用如下set方法:
public void set(int field, int value)
add()方法可以用来给某个字段加上一定的数。
get()方法,可以获得某个字段的值,与Date不同的是获得的星期,1是星期日、2是1,以此类推。
正则表达式
Java提供了java.util.regex
包,包含可以用于处理正则表达式的匹配操作的Pattern和Matcher
类。
正则表达式为何物?
一种包含普通字符和特殊字符的可以用来描述和匹配字符串的文本模式。
元字符和相关特性
字符匹配
普通字符,如”a”直接进行匹配;
元字符有特殊含义:如:\d
可匹配任意数字字符等价于[0-9]
;
\w
匹配任意字母数字字符;
.
匹配任意字符(除”\n”);
[\s\S]
匹配所有,\s
匹配所有空白符包括换行,\S
匹配非空白符,不包括换行。
量词
*
匹配前面的模式零或多+
匹配一或多?
匹配0或一注意:
*
和+
都是贪婪的,它们会尽可能多的匹配文字,只有在其后加上一个?
才可实现非贪婪或最小匹配。再相应表达式后加上?
可使表达式从”贪婪”变为”非贪婪”或最小匹配。{n}
匹配恰好n次{n,}
匹配至少n次{n,m}
匹配至少n次至多m次。
字符类
这里注意:由于
.
一些元字符已经形成了特定的模式,因此在使用的时候不再需要[]括起来了,也可以理解为那些元字符是已经自带中括号的模式了。
[]
匹配括号内任意一个字符(普通字符)[^ ]
:匹配除括号内的任意一个字符
边界匹配
^
匹配开头$
匹配结尾\b
匹配单词边界,若其位于要匹配字符串开头,则从开头开始匹配;结尾同理,单词边界是指\b
只从开头或结尾进行相应的查找与匹配。\B
匹配非单词边界,非单词边界指其只能从单词中间进行匹配,不能匹配单词的开头或结尾。
分组和捕获
()
用于分组和捕获子表达式,左右括号分别用来标记一个子表达式的开始和结束位置,子表达式可获取后使用,相邻选择项之间用|
分割,()
表示捕获分组,(?:)
用于分组但不捕获子表达式
特殊字符
\
转义字符,用于匹配特殊字符(例如:*
、?
等)本身
.:
匹配任意字符除了换行符
|:
指定多个模式的选择
输入输出
输出System.out
简单输出.println
,格式化输出.printf
(类似C语言中的占位符的用法)。System是java.lang包中的类,out是其静态变量,println是out对象提供的方法。
输入Scanner/System.in(Scanner需要在首行预处理import java.util.Scanner;
)
多种方法:nextLine
(读取一行输入并获取一个字符串,获取的是换行符前的所有字符)、nextInt
(读取一行输入并获取一个整型数)、nextDouble
(读取一行输入并获取一个双精度浮点数)、next()
(跳过遇到有效字符前的所有空白,输入有效字符后才将后续输入的空白(包括换行符)作为分隔符或结束符,不可得到带有空格的字符串,需要注意通过该方式读入整数浮点数或以空白结尾的字符串会留下换行符,这在之后还想使用nextLine()
获取字符串得需要先把换行符读入,再读入相关字符串),建议在输入前先使用hasNextXxx
方法判断是否有输入。
使用前要先创建Scanner对象:Scanner scanner = new Scanner(System.in);
之后就是调用这个scanner对象的方法来进行输入。
注意:Scanner由于只能输入可见字符,所以其并不适合从控制台读取密码,可以通过Console类采取下列代码读取一个密码:
1 |
|
为了安全起见,读入的内容要放入字符数组中,且读入处理后需要快速用一些填充值覆盖字符数组。
不过该代码在继承编译环境中会报空指针错误(NullPointerException),可以直接在命令提示符中采用javac XX.java,java XX的方式运行代码。
多种输入指令的处理
按照指令单独设计类CommandUtil(一个父类或接口)(提供command方法)。
按照指令处理要求设计若干个具体的实现子类,如AddBottle等。
再设计一个Manager类管理commandUtil对象。
JUnit检查输出是否正确的方法(在OO中请不要使用,会出现疑似非法行为的警告)
即先把把标准输出定向到一个 ByteArrayOutputStream
中去, 完后把这个流转成字符串来断言它的内容, 最后恢复标准输出为 System.out
, 代码如下:
1 |
|
赋值与拷贝
浅拷贝:对于对象中的引用类型属性,复制其引用值(地址值)。
单层拷贝,默认的clone
方法即为浅拷贝。
下列代码即为浅克隆实例:
1 |
|
其中dog1
引用了新创建的Dog实例,再次声明的dog2
变量重新引用了dog1
引用的实例,即这两者指向的是同一块内存空间,调用时处理的数据是同一份。
深拷贝
就浅拷贝中的实例,改为如下方式即可实现深拷贝:
1 |
|
需要自己实现。
容器的克隆
若需要对一个容器进行深克隆,一定要遍历容器中的所有对象,对每一个对象都进行深克隆。
当对一个容器进行深克隆时,若想判断是否真的对容器中的每个引用都进行了深克隆,可在调试器中看克隆前后对象的数字。若克隆前后对象的数字不同,且这两个对象的属性(包括容器/数组内容)后的数字均不同,则可认为彻底完成了对该对象的深克隆。
object ID这串数字是 JVM 上报的 objectID。他唯一标识了 JVM 中的对象。除非已显式处置 ObjectID(我们不会用到),否则不会重复使用 ObjectID 来标识不同的对象,而不管引用的对象是否已被垃圾回收。objectID 为 0 表示空对象。
Iterator(迭代器)
Iterator是Java集合框架中的一种机制,用于遍历集合(如列表、集合和映射等)的接口。
它本身并不是一个集合,而是一种用于访问集合的方法,可以用来迭代ArrayList和HashSet等集合。三个迭代器接口定义的最常用的方法是:next()
-返回迭代器的下一个元素,并将迭代器的指针移到下一个位置。hasNext()
-用于判断集合中是否还有下一个元素可以访问。
remove()
-从集合中删除迭代器最后访问的元素,采用迭代器后一般不对原集合进行操作,为了防止ConcurrentModificationException 异常
只对迭代器操作。
获取迭代器
1 |
|
使用迭代器遍历集合时,如果在遍历过程中对集合进行了修改(例如添加或删除元素),可能会导致 ConcurrentModificationException 异常
,为了避免这个问题,可以使用迭代器自身的 remove() 方法进行删除操作。
添加操作呢?
采用如下代码对集合内的元素进行遍历;
1 |
|
注意:迭代器是一种单向遍历机制,只能从前往后遍历集合中元素,不能往回遍历,使用迭代器遍历集合时不能直接修改集合中的元素。
java认为在迭代过程中,容器应该保持不变,因此,java容器中通常保留了一个域称为modCount,每次对容器进行修改,这个值都会+1,而调用iterator方法时,返回的迭代器会记住当前的modCount,随后迭代过程中会检查该值,一旦发现其发生改变,就会抛异常。而由于在迭代器的remove方法的内部实现中,修改modCount后会将其又赋值给exceptModCount,使这两者始终相等,因此不会报错:
1 |
|
由于foreach
内部实现本质就是迭代器,所以,下列方法在运行时也会报ConcurrentModificationException 异常
:
1 |
|
Java内存情况
JVM内存
虚拟机栈
栈中存放方法中的局部变量,方法的运行一定要在栈中进行。栈内存的数据用完就释放。简称栈,即JVM栈,Java虚拟机栈。
堆区
堆是Java虚拟机中最大的一块内存区,用于存储对象实例。省流:用户new出来的所有对象和数组,都是存放在堆中。
元空间
虚拟机加载的类信息、常量、各类方法的信息。
本地方法栈
程序计数器
常见bug
深浅克隆问题
对象: ==/equals?
这两者不相同,前者用来判断两者是否为同一个对象,即引用的对象是否相同,而后者仅用来判断两者的某些属性是否相等。
神奇的类:String
字符串对象,假设不通过new方法创造是被放在一个叫做“字符串常量池”的地方,并不是和我们创建对象所用的堆区在一起的。
对于String str = "12345;"
,当检测到”12345”时先会在字符串常量池中判断是否有该字符串,不存在就在字符串常量池中创造一个这样的对象,并将其引用传递回来。也就是说,”12345”==“12345”,这两个 “12345” 本身指代的就是同一个在字符串常量值里的对象。
Map接口的实现如何比较key的相等性
使用equals()
方法而不是==
。
Map接口的实现,尤其是HashMap此类基于哈希表的实现,需要能够准确地判断两个键是否等价(及内容相同),来正确地存储和检索键值对。因此会依赖于键对象地equals()
方法和hashCode()
方法来确保键地唯一性和等价性。
前者用来比较两个对象的内容是否相等。若两对象通过equals
方法比较为相等,则hashCode()
方法也必须返回相同的整数值。而后者用于获取对象的哈希码。
scanner.next系列
用法类似C语言里的scanf()
遍历容器:迭代器删除
- 遇到删除的不使用 for 循环,而使用迭代器遍历,删除的时候使用迭代器的删除方法
- 将删除和遍历分开,比如把要删除的对象用一个容器先存起来,遍历后统一删除。
index
String对象有一个方法为substring
,
1 |
|
设计模式
创建型模式
关注如何创建对象,核心思想:将对象的创建和使用相分离,使两者能相对独立地变换。
工厂方法
简单工厂模式
定义一个用于创建对象的接口,让子类决定实例化哪一个类,工厂方法使一个类的实例化延迟到其子类。将复杂的对象创建工作隐藏起来,仅暴露接口供客户使用。(有什么用?用户在输入时并不会想完全规定你的new方法的输入方式,需要一个特殊的接口类把需要产生的对象对应生成出来)
本质上使一种类创建型模式。在该模式中,可以根据参数的不同返回不同类的实例,其还专门定义了一个类负责创建其他类的实例,被创建的实例通常有相同的父类。
工厂实例化生成的产品的共同父类通常是抽象类(可理解为一个可包含普通方法和成员变量的接口,在被子类继承时可以选择性的重写抽象类的方法)。
1 |
|
在工厂类中定义一个对于不同需求实例化不同产品的方法,进而实现”按需分配“。
工厂模式
简单工厂模式是用一个厂生产多个产品,而工厂模式则是用多个厂生产不同的产品。工厂模式是由一个抽象工厂接口和多个实现工厂子类来实现。
抽象工厂模式
提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
Kit模式,一种对象创建型模式。
定义具体工厂的公共接口,让具体工厂决定创建哪种具体产品。
打破工厂和产品一对一关系,满足一个具体的工厂类可生产多个打类的产品。
建议将具体的关系图如上先草绘出来再动手编程。
单例
保证一个类仅有一个实例,并提供一个访问它的全局访问点,即自行实例化并向整个系统提供这个实例。
该类的构造方法一定是private
的(构造器私有化),且该实例是属于当前类的静态成员变量。例:
1 |
|
该类提供一个静态方法,能够向外界提供当前类的实例:
1 |
|
存在以下两种方式实现对该类单例的实例化:
在类加载时就进行实例化(饿汉式):
1 |
|
在第一次使用时进行实例化(懒汉式):
1 |
|
在多线程中上述实例化方法会导致在竞争条件下创建出多个实例,须对整个方法进行加锁:
1 |
|
结构型模式
行为型模式
观察者
定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
是一种通知机制,让发送通知一方与(被观察方)和接受通知一方(观察者)彼此分离,互不影响。通过多使用一个观察者接口将需要通知的对象与其他区别开来,并且将在需要观察物品的载体类中设置相应的改变方法(内附通知)。
例子:当关注的up主更新视频时,会收到更新通知:
1 |
|
命令
将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。
将命令的创建和执行分离,使得调用者无需关心具体的执行过程。
通过封装Command
对象,命令模式可保存已执行的命令,从而支持撤销、重做等操作。
一种用来批量化处理多种命令的模式:从控制台获得输入(输入的解析,识别不同指令的差异)、命令该交给哪个对象执行(封装成统一的命令接口,根据功能要求设计具体命令实现类)、继续封装,让Receiver角色的类封装命令的具体处理,命令对象聚合receiver对象、让一个专门的类接受输入,解析并代理给命令对象。
设计命令接口:按照指令处理要求设计若干具体的命令实现类BottleCommand等
设计一个具体处理命令的业务类:如Adventurer类
Invoker类管理m个comman对象:ArrayList< Command >, cmd = cmdList.get(…), cmd.execute(message)
Client类即为主类,创建invoker对象,创建命令处理类对象,创建receiver对象
策略
定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化。
顶层归纳类的共性行为职责,允许每个类采取不同的实现策略:
1.类有确定的行为职责,实现与其所管理的数据紧密相关
2.一组类的行为职责在概念上相同或相似,实现有差异
3.将差异实现为策略(独立的类),上层类通过聚合来引用
对策略进行封装后相互独立,可拓展性好,增加新的实现策略不影响客户代码和已有实现策略。
归一化的对象管理
1.顶层业务类设置业务容器管理业务对象
2.根据业务特征设计出多种业务类
3.层层代理设计方案避免跨层依赖(关键在于顶层业务类是否需要区分不同类型的业务对象进行分别处理)
4.顶层业务类只进行基础业务类,各种具体业务类对象可以统一纳入基础业务类的管理范畴(即归一化)
多线程
进程包含线程,但多任务即可由多进程实现也可由单进程内多线程实现,还可混合多进程+多线程。具体情况具体分析。
相较于多线程,多进程缺点有:1.创建开销大,尤其是Windows
。2.进程间通信慢于线程间,线程间只需读写同一变量即可。
但是,多进程稳定性强于多线程,一个进程崩溃不影响其他,多线程下则不同,唇亡齿寒。
创建新线程
通过实例化一个Thread
实例,并调用其start()
方法来创建一个新线程。
方法一:从Thread
派生一个自定义类,再重写run()
方法来做点什么:
1 |
|
方法二:创建Thread
实例时,传入一个实现Runnable
接口的实例:
1 |
|
为了模拟并发执行的效果,可以在线程中调用Thread.sleep()
来强迫当前线程暂停一段时间,其中sleep()
传入的是毫秒。
Tips:直接调用
Thread
实例的run()
方法是无效的,这相当于调用了一个普通的Java方法,当前线程并没任何改变,并不会启动新线程,必须调用Thread实例的start()方法才能启动新线程。
可以设定线程优先级:
Thread.setPriority(int n)
。
线程状态
NEW
:新创建的线程,尚未执行。Runnable
:运行中的线程,正在执行run()
方法的Java代码。Running
:正在运行的线程。Blocked
:运行中的线程,因某些操作被阻塞挂起。Waiting
:因某些操作而在等待中。Timed_Waiting
:因执行sleep()
方法正在计时等待的线程。Terminated
:终止的线程,即run()
方法执行完毕。终止原因大致有三种:1.正常终止:run()
执行到return
语句;2.意外终止:run()
因尾部或许的异常导致线程终止;3.对某个线程的Thread
实例调用stop()
方法强制终止。
一个线程还可以等待另一个线程直到其运行结束,想像你在进程调度过程中强行插入一个,例如:main
线程在启动t
线程后,可通过t.join()
等待t
线程结束后再继续运行。也就是join
就是指等待调用该方法的进程结束后再继续往下执行自身线程。
此外,join(long)
的重载方法可指定一个等待时间,超过该时间就不再继续等待了。
中断线程
只需要在其他线程中对目标线程调用interrupt()
方法并由目标线程反复检测自身状态是否是interrupted
状态,是就立刻结束运行即可中断线程。
不过调用interrupt()
方法只是发送了”中断请求”,具体是否结束进程还要看具体实现,在run()
方法内可以通过isInterrupted()
方法来获取当前是否被interrupt
的状态值,进而通过相应代码完成对于interrupt
的响应。
若线程处于等待状态,例如,t.join()
让main
线程进入等待状态,此时,若对main
线程调用interrupt()
,join()
方法将立刻抛出InterruptedException
,这说明了有其他线程对其调用了interrupt()
方法,一般情况下在具体实现中需要设置try...catch
异常处理逻辑来结束该线程的运行。
另一种常用中断线程的方法是设置标志位,常用一个running
标志位来标识线程是否应该继续运行,在外部线程中,通过将HelloThread.running
置为false
,就可结束线程。其实本质上还是通过线程状态量的改变在线程run()
方法内部的实现中完成相应逻辑上的判断进而结束线程。
实例代码:
1 |
|
守护线程
为其他线程服务的线程称为守护线程,JVM中,所有非守护线程执行完后,无论是否有守护线程,虚拟机都自动退出,因此,JVM并不管守护线程的状态,而守护线程中编写代码不能使之持有任何需要关闭的资源,如打开文件。
那么该如何创建守护线程呢?
1 |
|
线程同步
synchronized
本质上是解决多个线程同时读写共享变量出现的数据不一致的问题。
也就是通过加锁和解锁的方式保证一段代码的原子性(执行过程中只有一个线程可以访问)。加锁和解锁间的代码块被称为临界区,任何时候临界区最多都智能有一个线程能执行。通过使用synchronized
关键字对一个对象加锁,保证代码块在任意时刻至多有只有一个线程能执行。例如:
1 |
|
大致使用方法:
1.找出修改共享变量的线程代码块。
2.选一个共享实例作为锁。
3.使用synchronized(lockObject) {...}
由于JVM只保证了同一把锁在任意时刻只能由一个线程获取,但两个不同的锁在同一时刻是可以被两个线程分别获取的,因此对于同一共享变量的修改,最好只提供一把锁,防止上述情况的发生。而对于不同的且不存在数据竞争的共享变量的修改,提供对应数量的锁,可以最大化的提高执行效率。
不需要synchronized的操作
- 基本类型(一般
long
、double
除外)赋值 - 引用类型赋值
- 多线程连续读写多个变量时,同步的目的是保证逻辑的正确性。
- 不可变对象无需同步,因为并不会修改对象状态。
同步方法
让线程自行决定锁对象往往会使代码混乱,不利于封装,更好的方法是将synchronized
逻辑封装起来,如:
1 |
|
也就是synchronized
只对最底层的需要原子化的操作进行加锁。
注意到,synchronized
锁住的是this
对象,即当前实例,因此创建多个实例时,它们间互不影响,可并发执行。
若一个类被设计允许多线程正确访问,则该类是”线程安全”的,Java标准库的java.lang.StringBuffer
、一些不变类(String
、Integer
、LocalDate
等所有成员变量都为final
)以及Math
这种只提供静态方法的类都是线程安全的。
但是一些动态容器是非线程安全的,如ArrayList
、HashMap
、HashSet
、LinkedList
和ArrayDeque
等。
当锁住的是this
实例时,实际上也可以用synchronized
修饰该方法,被其修饰的方法是同步方法,表示整个方法都须用this
实例加锁,即下列两种方式等价:
1 |
|
那么若对一个静态方法添加synchronized
修饰符时,由于没有this
实例,则锁住的是每个类由JVM自动创建的Class
实例。
死锁
Java线程锁是可重入的锁,也就是JVM允许同一线程重复获取同一个锁,即可重入锁。
死锁是多线程各自持有不同的锁,并互相试图获取对方已持有的锁,导致了无限等待。
例如:
1 |
|
只有规定好线程获取锁的顺序,才可避免死锁。
使用wait和notify
对于多线程运行来说,线程内若不满足运行条件,应该交出锁进入等待状态而非霸占着锁,原则应是条件不满足时,线程进入等待状态;反之,线程唤醒,继续执行任务。
示例:
1 |
|
注意:wait()方法必须在当前获取的锁对象上调用,只能由锁住的对象来调用,可不是由线程来调用。这里的wait()方法在调用时会释放线程获得的锁,而返回时,线程又会重新试图获得锁。notify()
和notifyAll()
方法都必须在以获得的锁对象上调用。
使用ReadWriteLock
对于临界区内的内容,我们希望做到:允许多个线程同时读,但只要有一个线程在写,其他线程就必须等待。
ReadWriteLock
可解决该问题,其保证:1.只允许一个线程写入;2.无写入时,多个线程允许同时读。
我们可以通过一个实例化的ReadWriteLock
实例获取读锁和写锁:
1 |
|
使用Semaphore
为了确保同一时刻最多只有N个线程可以访问某一受限资源,可使用Semaphore
,例:
1 |
|
此外,还可以通过设定等待时间tryAcquire()
的方法来获取semaphore
:
1 |
|
使用Concurrent集合
interface | non-thread-safe | thread-safe |
---|---|---|
List | ArrayList | CopyOnWriteArrayList |
Map | HashMap | ConcurrentHashMap |
Set | HashSet / TreeSet | CopyOnWriteArraySet |
Queue | ArrayDeque / LinkedList | ArrayBlockingQueue / LinkedBlockingQueue |
Deque | ArrayDeque / LinkedList | LinkedBlockingDeque |
尽量使用Java标准库提供的并发集合。
JML
一种行为接口规格语言,基于Larch方法构建。
主要用法:1.开展规格化设计。2.针对已有代码实现书写对应规格,提高代码可维护性。3.基于源码与规格实现,设计覆盖更好的自动化测试,自动对测试执行结果判断,形成测试预言。
注释结构
以javadoc
注释表示规格,每行都以@起头。有行注释(//@annotation
)和块注释(/*@ annotation @*/
)两种,一般被放置在被注释部分的紧邻上部。
/*@ pure @*/
修饰的方法,是**”纯粹的”,也就是其执行无任何副作用,类中的方法规格必须建立在其管理的数据规格**上。
requires
定义方法的前置条件;副作用起范围限定作用,assignable
表明方法能赋值相应类成员属性,modifiable
表明方法能修改相应类成员属性,\nothing
表示方法不产生任何副作用是pure
方法,\everything
表示对所有类成员都可修改;ensures
定义后置条件。
注意,规格中每个子句都须以;
结尾,否则JML工具将无法完成解析。
规格变量的声明,分为静态或实例两种。若在interface
中声明,要求明确其类别。对于//@ public model non_null int [] elements;
,假如是静态的,应为~ public static model ~
;若为实例,应声明为~ public instance ~
。不声明默认是实例型。
JML表达式
对java的扩展,新增一些操作符和原子表达式,但是新增部分仅用于JML的assert
和其他相关注释体,JML断言中,不可带有赋值语义的操作符,如++
等。
原子表达式
\result
表示一个非void
方法执行后的返回值,注意:获取的是注释的方法的返回值。\old(expr)
表示表达式expr
在注释方法执行前的取值,其遵循java
引用规则,对一个对象引用而言,只能判断引用本身是否改变,不能判断指向对象实体内容是否改变。任何情况下,都应使用\old
将关心的表达式取值整体括起来。\not_assigned(x,y,...)
表示括号中的变量是否在方法执行过程中被赋值,如未被赋值,返回true
;否则false
。一般用于后置条件的约束表示上。\not_modified(x,y,...)
和上一个类似。\nonnullelements(container)
表示container
对象中存储的对象无null
,等价于1
2container != null &&
(\forall int i; 0 <= i && i < container.length; container[i] != null);\type(type)
返回**类型type
**对应的类型,如:\type(boolean)
为Boolean.TYPE
,TYPE
等同于java
中的java.lang.Class
。\typeof(expr)
返回expr
对应的准确类型,如\typeof(false)
为Boolean.TYPE
。
量化表达式
\forall
,全称量词修饰的表达式,表示对给定范围内的元素,都满足的约束。\exists
,存在量词修饰的表达式。\sum
返回给定范围的表达式的和。例如(\sum int i; 0 <= i && i < 5; i)
表示从0加到4,两个分号间代表范围,第二个分号后代表求和的表达式。\product
返回给定范围内表达式连乘结果。语法类似\sum
。\max
返回给定范围表达式最大值。\min
返回最小值。\num_of
返回指定变量中满足相应条件的取值个数,此处范围仍在两个分号之间,相应条件在第二个分号之后。不失一般性,\num_of
可写成(\num_of T x; R(x); P(x))
,T为x的类型,R(x)为x取值范围,P(x)定义了x满足的约束条件。也等价于(\sum T x; R(x) && P(x); 1)
.
集合表达式
可在JML规格中构造一个局部的集合(容器),明确集合中可包含元素。如:new JMLObjectSet {Integer i | s.contains(i) && 0 < i.intValue() }
表示构造一个JMLObjectSet
对象,其中包含元素类型是,且集合中所有元素都在容器集合s中出现,且数值大于0。
集合构造形式为:new ST { T x | R(x) && P(x)}
,R(x)
对应集合中x
的范围,常来自某个既有集合中的元素,P(x)
对应于约束。
操作符
不同于java
的四类操作符:
- 子类型关系操作符:
E1<:E2
,若E1
是E2
的子类型,结果为true
。相同类型也为true
。 - 等价关系操作符:
b_expr1<==>b_expr2
或b_expr1<=!=>b_expr2
,其中b_expr1
和b_expr2
都是布尔表达式,其意思为b_expr1==b_expr2
或b_expr1!=b_expr2
。 - 推理操作符:
b_expr1==>b_expr2
或者b_expr2<==b_expr1
,运算方式同蕴含。 - 变量引用操作符:
\nothing
指示空集,\everything
指示全集,包括当前作用域下能访问的所有变量。
方法规格
包括前置条件、副作用和后置条件。
区分两种方法:全部过程(前置条件为恒真,适用任意场景)、局部过程(非恒真前置条件,要求调用者确保调用时满足相应前置条件)。
针对所有输入可能,需设计正常行为规格和**异常~**。
前置条件
requires
子句表示。注意,多个requires
子句是并列关系之间是&
关系,需要在单个子句中添加||
实现或逻辑:requires P1 || P2;
。
后置条件
ensures
子句表示,并列情况和或逻辑的实现同前置条件。
副作用范围限定
指方法执行过程中会修改对象的属性数据或类的静态成员数据。例如:@ assignable elements, max, min;
注意,JML不允许在副作用子句中指定规格声明的变量数据。一般来说,方法规格对调用者可见,但是方法所在类的成员变量一般对调用者不可见,而有时方法规格不得不使用类的成员变量来限制方法的行为,与私有化产生矛盾,JML提供了/*@ spec_public @*/
来注释一个类的私有成员变量,表示其在规格中可直接使用。
在方法规格中是可以引用pure
方法返回的结果的:
1 |
|
区分方法的正常功能行为和异常行为
采用public normal_behavior
表示接下来部分是对方法正常功能给出的规格。正常功能:输入或方法关联this
对象的状态在正常范围内所指向的功能。与之相反的就是异常功能,public_exceptional_behavior
下定义的规格。若方法无异常处理,那么便无需区分这两者。这两者间有一个also
关键字用于分隔,另起一行书写,此外其还会用在父类中对相应方法定义了规格,子类重写了该方法,需补充规格时,在补充的规格前须使用also。
一个重要的设计原则,同一方法的正常功能前置条件和异常功能的一定不能有重叠。不同于正常的后置条件,异常的后置条件常表示为抛出异常,用signals
子句表示,当然也可以正常使用ensures
子句描述 方法执行产生的其他结果。
signals
子句结构为signals(*** Exception e) b_expr
,b_expr
为true
时抛出异常e
。这里的e
既可以是java的,也可以是用户定义的。
注意,若一个方法运行时会抛出异常,一定要在方法声明中明确指出,throws
,且确保signals中给出的异常类型一定等同于方法声明中给出的异常类型,或是其子类型。
还有一种简化版,signals_only
子句,后跟异常类型,强调满足前置条件就抛出异常,例:
1 |
|
类型规格
针对数据类型设计的限制规则,一般是针对类或接口设计的约束规则。
主要的两种限制:**不变式限制(invariant)和约束限制(constraints)**。
invariant
要求在所有可见状态下必须满足的特性,语法:invariant P
。
可见状态一般包含以下几种,对于对象o来说:
- 对象的有状态构造方法(用于初始化对象成员变量初值)的执行结束时刻。
- 调用一个对象回收方法(finalize方法)释放相关资源开始的时刻。
- 调用对象o的非静态、有状态方法(non-helper)的开始结束时刻。
- 调用对象o对应的类或父类的静态、有状态方法的始末时刻。
- 未处于对象o的构造方法、回收方法、非静态方法被调用过程中的任意时刻。
- 未处于对象o对应类或父类的静态方法被调用过程中的任意时刻。
省流:凡修改成员变量(静态、非静态)的方法执行期间,对象状态均不可见,这里指带有完整可见的意思。
invariant
强调任意可见状态下都要满足的约束。不变式中是可以直接引用pure形态的方法。与规格变量声明相同,类变量成员的是否静态之分,有static invariant
和instance invariant
,其中前者只针对静态成员变量约束,后者可针对静态和非静态变量进行约束。
constraint
对象状态变化时也要满足一些约束,本质上也是不变式,一般对前序可见状态和当前可见状态的关系的约束。
例:
1 |
|
invariant
和constraint
可直接被子类继承获得。
同样的,也有static constraint
和instance constraint
前者只涉及静态成员变量,而后者可都涉及。
方法与类型规格的关系
对于不可变类来说,不需要定义不变式,只要在构造方法中明确其初始状态应满足的后置条件即可。
invariant
、constraint
与类的方法的关系:
静态成员初始化 | 有状态静态方法 | 有状态构造方法 | 有状态非静态方法 | |
---|---|---|---|---|
static invariant | 建立 | 保持 | 保持 | 保持 |
instance invariant | (无关) | (无关) | 建立 | 保持,除非是finalize 方法 |
static constraint | (无关) | 遵从 | 遵从 | 遵从 |
instance constraint | (无关) | (无关) | (无关) | 遵从 |
建立指静态成员建立了满足相应不变式的类或对象状态。保持指若方法执行前不变式满足,执行后仍应满足。遵从表示前序可见状态与当前可见状态满足约束。