阶段一:Java基础
学习Java的基本语法、面向对象编程(OOP)以及Java集合框架。
理解Java的异常处理机制、IO操作、多线程编程和反射等高级特性。
学习Java的设计模式,如单例模式、工厂模式、观察者模式等。
Lecture1
面向对象
- 将实际问题分解为不同的对象
- 不同的对象提供不同的服务
- 对象之间可以传递消息
面向对象语言
- 编程语言直接提供了对对象的支持
- 定义对象 , 对象提供的服务 , 消息传递方法
- 优点
- 缩短实际问题到计算机算法的距离
面向对象编程要素
- 任何事物都是对象
- 程序为一些对象之间的相互协作
- 一个对象可以包含另一个对象
- 每个对象都有类型
- 一种类型的对象接收相同类型的消息,提供相同类型的服务
对象的基本要素
- 状态
- 行为
- 类型
利用已有类型定义新的类型
- 复用
- 组合
- 继承
Lecture2
面向对象编程的基本步骤
- 定义类型
- 创建属于该类型的对象
- 使用对象的服务
定义Java类
class MyType{
//数据
int i;
double d;
char c;
//方法
void set(double d);
double get();
}
创建类的对象
MyType a = new MyType();
使用对象的方法
int b = a.i;
a.set(0.5);
a.get();
销毁对象
- Java语言会自动销毁对象
引用
- 引用是受限的指针
- 体现在不允许使用指针运算,以及无法强制类型转换
不可变类型
- 类型的对象一旦创建就不能被改变
- 例子 String 类 , Integer 类 , Float 类 ...
对象存储位置
- 基本类型存储在栈内存
- 类存放在堆内存
Lecture3
强制转换操作
- 基本类型
- 自动转换 : 当转换是安全的 ( 例如 int 转为 double)
- 显式转换 : 当转换将损失精度 ( 例如 double 转 int)
- boolean 类型 不能强制转换
- 类
- 一般不允许强制转换(特殊情况下的继承类可以)
循环结构foreach
int [ ]a = {1, 2, 3, 4, 5};
for (int i : a)
System.out.println(i);
静态方法
- 定义时加关键字static
public class MyType { int i; double d; char c; void set(double x) { d = x; } double get() { return d; } //静态方法 public static void main(String [ ]args) { System.out.println(“Hello”); } }
- 不用创建对象就可以被调用的方法
- 也称为:类方法
静态数据
- 类似于静态方法,不需要实例化即可被调用
- 可以在类的不同对象中共享
默认访问控制
- 方法默认private
- 接口默认public
库
- 一组功能相关的类,为其他用户提供服务
模块化编程
- 将任务分解成为简单 , 更容易管理的子任务
- 优点
-
- 简单
-
- 易于debug
-
- 代码重用
-
- 易于维护
Lecture4
函数重载
- 根据参数的类型和数量确定方法,而非函数名
- 优点:接口简洁统一
- 返回值无法重载
自动装箱/拆箱
考虑如下代码
//在-128~127 之外的数
Integer i1 =200;
Integer i2 =200;
System.out.println("i1==i2: "+(i1==i2));
// 在-128~127 之内的数
Integer i3 =100;
Integer i4 =100;
System.out.println("i3==i4: "+(i3==i4));
输出
i1==i2: false
i3==i4: true
Integer.java源码
public static Integer valueOf(int i) {
if(i >= -128 && i <= 127)
return IntegerCache.cache[i + 128];
else
return new Integer(i);
}
对于–128到127(默认是127)之间的值,Integer.valueOf(int i) 返回的是缓存的Integer对象(并不是新建对象)
所以范例中,i3 与 i4实际上是指向同一个对象。
而其他值,执行Integer.valueOf(int i) 返回的是一个新建的 Integer对象,所以范例中,i1与i2 指向的是不同的对象。
当然,当不使用自动装箱功能的时候,情况与普通类对象一样。
this关键字
- 在非静态方法中,返回调用该方法对象的引用
- 特殊情况,调用构造函数
public class MyType { int i; double d; char c; void set(double x) { d = x; } double get() { return d; } MyType(double d) {this.d = d;} MyType(int i) {this.i = i;} MyType(int i, double d, char c){ this(d); this.i = i; // can not use this(i) again this.c = c; } public static void main(String [ ]args) { MyType m = new MyType(); m.set(1); } }
- 出现在构造函数第一行
- 只能调用一个构造函数
销毁对象
-
根据对象占有的资源决定自动/显式回收
-
- new 操作时系统分配的内存(系统自动回收)
-
- 其他资源(程序显式申请)
-
垃圾回收不回收程序显式申请的资源
public class Resource { int i; BufferedReader f; Resource() { i = 0;} void open() { f= new BufferedReader(new FileReader(new File(“a.txt”))); } String readLine() { return f.readline();} void close() { f.close(); } public static void main(String [ ]args) { Resource r = new Resource(); r.open(); //… r.close();//手动回收 } }
-
垃圾回收
-
- 对象的引用数,指指向该对象的引用的个数
-
- 引用数为0时自动回收
String s = new String(“hello”); String t = new String(“world”); s = t;
- 引用数为0时自动回收
-
- 内容为 ''hello'' 的对象可回收,但不会立即被回收
-
- 当 Java 虚拟机 (JVM) 发现内存不够时尝试进行回收
-
- 由 JVM 决定是否回收 , 何时回收 ( 并非实时进行 )
-
- 原因:垃圾回收将占用系统资源,影响用户程序;减少回收频率;
-
- System.gc() 调用:通知 JVM 可以进行垃圾回收
-
- 类的 finalize() 方法(避免使用)
JVM实现垃圾回收
算法1:引用计数
- 每个对象包含一个计数器,记录指向改对象的引用数
- 垃圾收集器检查所有对象,如果引用数为0则删除
- 缺点
-
- 循环引用
Dog newDog = new Dog(); Tail newTail = new Tail(); newDog.tail = newTail; newTail.dog = newDog;
在这里,newTail中拿着对newDog的引用,newDog中拿着对newTail的引用。如果newDog要被回收,前提是newTail被先回收,这样才能释放对newDog的引用。但是反回过来,newTail要被回收的前提是newDog要被先回收。当buildDog函数退出后,看起来垃圾回收管理似乎就始终无法回收这两个实际已经不再需要的对象。
- 循环引用
算法2:stop-and-copy
- 找出所有有效的对象(从栈上的引用出发)
- 每找到一个有效对象,将其拷贝到另外一块内存区域
- 修改所有引用的值
- 被垃圾搜集的程序将被停止
- 缺点
这种“复制式回收器”效率会降低,有两个原因,
首先,要有两个堆,然后在这两个堆之间倒腾,从而得维护比实际需要的多一倍的时间。
在java虚拟机对此问题的处理方式是,分配几块较大的内存,复制动作发生在这些大的内存块之间。
算法3:mark-and-sweep
这种体制同样也要先暂停程序才能进行,它的思想同样追随引用,从堆栈或静态存储区出发,遍历所有的引用,进而找出所有存活对象,找到一个存活对象就标记一个。这个过程不会发生回收任何对象,而是全部标记工作完成后,才会清理未被标记的对象。这个过程不会发生任何复制动作,所以剩下的堆空间是不连续的,垃圾回收器如果希望得到连续空间的话,要重新整理剩下的对象。
相对的,当程序产生垃圾较多时,效率较低。
算法4:二者结合
- 当较容易产生垃圾时: 使用 stop-and-copy
- 当不容易产生垃圾时: 使用 mark-and-sweep
- 判断情况
-
- 大多数对象的生存周期较短
-
- 每个对象记录生存周期
-
- 根据生存周期归类
-
- 生存周期较小的对象 : 使用 stop-and-copy
-
- 生存周期较大的对象 : 使用 mark-and-sweep
初始化的顺序
- 所有数据成员初始化在构造函数调用前完成
- 按照定义的顺序初始化
静态成员初始化
- 当第一个该类型的对象被创建时初始化
- 后续创建该类型的对象时不初始化
- 先初始化静态数据成员 , 后初始化对象数据成员
Lecture5
Java包
- 由多个类组成
- 多个类共享一个命名空间
创建包
- package语句
-
- 指定当前 java 文件中的类属于哪一个包
- 包的结构与文件目录结构一致
//.\com\runoob\test\Runoob //文件名:Runoob.java package com.runoob.test
访问控制
- 类 : 数据 + 方法
- 公开的数据和方法
● 提供服务
● 所有用户都可以使用
● 需要保持稳定 , 不经常变化 - 隐藏的数据和方法
● 细节的 , 辅助性的方法和数据
● 不向用户公开
● 易变
package access
- 一个类的成员被标识为 package access
-
- 同一个包中的类可以访问
-
- 其他包中的类不能访问
- 没有标识符
- 如果没有 package 语句 , java 默认当前目录中的 java文件属于同一个包
public
- 类的成员标识为 public,则所有用户都能访问该成员
private
- 类的成员标识为 private,则除了该类自身外的所有类都无法访问该成员
protected
- 开放继承类的权限
权限控制表
修饰词 | 本类 | 同一个包的类 | 继承类 | 其他类 |
---|---|---|---|---|
private | √ | × | × | × |
无(默认) | √ | √ | × | × |
protected | √ | √ | √ | × |
public | √ | √ | √ | √ |
类的访问控制
- public class
-
- 每个 .java 文件包含一个 public class
-
- 该 class 的名字等于 .java 文件名
- package access class
-
- 每个 .java 文件中除去 public class 外 , 其他的 class为 package access
封装
- 访问控制的设计
public
- 提供服务,稳定
- 修改 public 成员将影响用户程序
private
-
提供辅助,易变
-
修改 private 成员是安全的
-
满足需求的情况下,接口尽可能简单
-
尽量使用 private 访问控制
Lecture6
类的复用
- 组合
- 继承
组合
- 将已有类的对象作为新类的数据成员
//MyType
class MyType {
public int i;
public double d;
public char c;
public void set(double x) { d = x;}
public double get() { return d; }
}
//MyCompType
public class MyCompType {
private MyType m = new MyType();
private String s;
public MyCompType(){
s = new String(“Hello”);
}
}
继承
- 子类有父类所有的方法和数据
class MyType {
public int i;
public double d;
public char c;
public void set(double x) { d = x;}
public double get() { return d; }
}
public class MySubType extends MyType{
public static void main(String []args){
MySubType ms = new MySubType();
ms.set(1.0);
System.out.println(ms.get());
System.out.println(ms.i);
}
}
- 子类可以定义新的方法和数据
- 子类可以更新父类的方法,称为重写 (overriding)
- 定义子类时,创建一个新的类,包含一个父类的对象作为数据成员
public class MySubType extends MyType{
/*
public MyType m;
*/
public string s;
public childMethods() {...}
}
- 子类的对象包含一个隐藏的父类对象,通过 super 引用父类的对象
- 当方法被重写时,可以通过 super 调用父类的方法
Lecture7
upcasting
- 不同类型的对象之间无法做赋值运算
- 子类类型的对象可以赋值给父类
- 子类是一种父类 “is-a”关系
- upcasting 例子
class Instrument {
public void play() {}
static void tune(Instrument i) {
// ...
i.play();
// ...
}
}
public class Wind extends Instrument {
public static void main(String[] args) {
Wind flute = new Wind();
Instrument.tune(flute);
}
}
- 上面例子中,flute是Instrument子类的引用,可以当作Instrument类型的引用
- 需要父类对象的地方可以用子类对象带入
- 一种安全的类型转换
downcasting
- 父类对象的引用赋值给子类对象的引用
public class MySubType extends MyType {
public void set(double x) {
System.out.println("sub class");
d = x;
}
public static void main(String[] args) {
MySubType ms = new MySubType();
ms.set(1.0);
MyType m = new MyType();
//downcasting 无法通过编译
MySubType n = (MySubType) m;
n.set(1.0);
}
}
- 可以通过编译的情况:将子类对象的引用先赋值给父类类型的引用,然后将此变量强制类型转换为子类对象
public class MySubType extends MyType {
public void set(double x) {
System.out.println("sub class");
d = x;
}
public static void main(String[] args) {
MySubType ms = new MySubType();
ms.set(1.0);
MyType m = ms;
m.set(1.0);
//downcasting 可以通过编译
MySubType n = (MySubType) m;
n.set(1.0);
}
}
- 总结:父类的引用可以指向子类的对象
final关键字
- 基本意义为:无法改变
- 一旦被赋值就无法被修改
class MyType {
public int i;
public final double d = 1;
public char c;
public double get() { return d; }
public void set(double x) {d = x;}
public static void main(String []args){
MyType m = new MyType();
// m.d = 2.0; error
}
}
使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升。在最近的Java版本中,不需要使用final方法进行这些优化了。
final关键字修饰方法,它表示该方法不能被覆盖(重写)。另外,类中所有的private方法都隐式地指定为是final的,由于无法在类外使用private方法,所以也就无法覆盖它。此时可以在子类中定义相同的方法名和参数,这种情况不再产生重写与final的矛盾,而是在子类中重新定义了新的方法。可以对private方法添加final修饰符,但并没有添加任何额外意义。
final关键字修饰类,表示该类无法被继承。
对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。
-
编译期间为常量,而非引用
-
引用变量被final修饰之后,虽然不能再指向其他对象,但是它指向的对象的内容是可变的
-
final 成员在定义时可以不给初值,但必须在构造函数中初始化
Lecture8
upcasting简化接口
class Instrument {
public void play(int note) {
System.out.println(“Instrument.play()” + n);
}
}
public class Wind extends Instrument {
public void play(int note) {
System.out.println(“Wind.play()” + n);
}
}
public class Stringed extends Instrument {
public void play(int note) {
System.out.println(“Stringed.play()” + n);
}
}
public class Brass extends Instrument {
public void play(int note) {
System.out.println(“Brass.play()” + n);
}
}
//Without upcasting
/*
public class Music {
public static void tune(Wind i) {
i.play();
}
public static void tune(Stringed i) {
i.play();
}
public static void tune(Brass i) {
i.play();
}
public static void main(String []args){
Wind flute = new Wind();
Stringed violin = new Stringed();
Brass frenchHorn = new Brass();
tune(flute);
tune(violin);
tune(frenchHorn);
}
}*/
//With upcasting
/*
public class Music {
public static void tune(Wind i) {
i.play();
}
public static void tune(Stringed i) {
i.play();
}
public static void tune(Brass i) {
i.play();
}
public static void main(String []args){
Wind flute = new Wind();
Stringed violin = new Stringed();
Brass frenchHorn = new Brass();
tune(flute);
tune(violin);
tune(frenchHorn);
}
}
*/
- 接口更加简洁
- tune方法能正确调用对应的重写 (override) 后的子类方法
多态
- 如上例,参数 Instrument i 可以代表不同的子类 , 并能正确调用它们的方法 ( 即有多种表现形态)
- 静态绑定(static binding):函数的位置在编译时确定
- 动态绑定(dynamic binding):函数的位置在运行时才能确定
upcasting + 多态带来的扩展性
- 增加新的接口不影响原有的只依赖于旧接口的代码
动态绑定
- Java中所有的方法都采用动态绑定,除了static和final
- 数据成员不使用动态绑定
- 构造函数中不能使用重写函数
Lecture9
抽象类
抽象方法
- 仅提供方法的名称,参数列表,和返回值
- 没有具体实现
- 使用abstract关键字
abstract class Instrument{
public abstract void play(int note);
}
- 无法直接创建抽象类的对象
- 抽象方法需要在子类中补足定义才有意义
- 若子类没有重写父类中的抽象方法,子类仍为抽象类
接口
- 所有方法都是抽象方法
- 只有方法的名称,参数和返回值
- 没有方法的实现
interface Instrument {
void play(int note) ;
String what();
}
-
没有代码重用,只保留upcasting和多态
-
所有实现该接口的类都具有接口提供的方法
-
任何使用该接口类型的方法 , 都可以使用他的任何一种实现
class Stringed implements Instrument {
public void play(int note) {
System.out.println(“Stringed.play()” + n);
}
public String what() {return “Stringed”;}
}
- 所有方法默认为public
- 所有数据默认为final static
实现多个接口
interface Plane {
void fly();
}
interface Boat {
void sail();
}
class Seaplane implements Plane, Boat {
public void fly(){
System.out.println(“Fly!”);
}
public void sail(){
System.out.println(“Sail!”);
}
}
-
多继承问题:一个类的父类只能有一个普通类/抽象类
-
一个类无法继承拥有不同函数(却有相同函数名)的不同接口
interface I1 {
void f();
}
interface I2 {
void f();
}
interface I3 {
void f(int i);
}
interface I4 {
int f();
}
class C1 implements I1, I2{
public void f() {}
}
class C2 implements I1, I3{
public void f() {}
public void f(int i) {}
}
/* compile error: return type incompatible
class C2 implements I1, I4{
public void f() {}
}
*/
扩展接口
interface A {
…
}
interface B extends A{
…
}
interface D {
…
}
interface D extends A, C{
…
}
接口适配器
class Person{
public void walk(){}
public void buyTicket(){}
public void takeFlight(){}
}
class PersonAdapter implements CanFly{
private Person p;
public PersonAdapter(Person p){
this.p = p;
}
public void fly(){
p.buyTicket();
p.takeFlight();
}
}
interface CanFly {
void fly();
}
class Adventure {
public static void travel(CanFly c) {
c.fly();
}
public static void main(String []args){
Bird b = new Bird();
Insect ins = new Insect();
travel(b); travel(ins);
Person p = new Person();
PersonAdapter pd = new PersonAdapter(p);
travel(pd);
}
}
- 在PersonAdapter中组合Person和接口方法,使得接口方法能够使用Person这一类型
工厂模式
- 定义一个Factory接口,通过这个接口,根据用户需求创建类
Lecture10
内部类
- 定义在一个类的内部,与组合不同
内部类与外部类的关系
-
内部类隐藏了一个指向外部类对象的引用
-
访问外部类对象的引用:OuterClassName.this
-
在外部类中创建引用:直接创建
-
在其他地方创建内部类的引用:OuterClassName.new
-
内部类通常实现某个接口/继承某个类
定义在某作用域中的内部类
- 作用域外不可见
- 也称为local inner class
匿名类
- 没有名字的内部类
- 必须继承某个类,或者某个接口
public class Parcel{
public Contents contents(){
return new Contents() {
// anonymous inner class definition
private int i = 11;
public int value() {return i;}
};
}
public static void main(String []args){
Parcel p = new Parcel();
Contents c = p.contents();
}
}
- 必须使用final的外部变量对成员进行初始化(可以不初始化,但初始化必须要final)
嵌套类
- 不包含指向外部类对象的引用
- 无法访问外部类的非静态成员
内部类的作用
-
多继承
-
可以通过多个内部类继承多个类/接口
-
同一个内部类可以有多个实例 , 每个实例有不同的状态
-
对同一接口 , 可以有不同的内部类实现
-
创建内部类对象可以按需创建
-
不必遵从 is-a 关系
Lecture11
容器
数组
int [ ] a = new int[]{1,2,3};
MyType [ ] b = new MyType[3];
MyType [ ] c = new MyType[3] {
new MyType(),
new MyType(),
new MyType()
};
- 长度不可变,无法添加删除元素
ArrayList
import java.util.*;
ArrayList<String> a = new ArrayList<String>();
// 插入 add(Object o)
a.add(“rat”); a.add(“cat”); a.add(“dog”); a.add(“dog”);
// 查询 contains (Object o)
System.out.println(a.contains(“cat”));
// 删除 remove(Object o) ( 若不在 List 中 , 返回 false, 否则返回 true)
a.remove(“dog”); a.remove(“dag”);
// 访问第 i 个元素 : get(int)
a.get(0);
// 对象的数量 : size()
a.size();
// 序号 indexOf
a.indexOf(“cat”);
// 子表 subList(int fromIndex, int toIndex)
List<String> sub = a.subList(2, 3);
// 是否为空 isEmpty()
System.out.println(a.isEmpty());
// 返回迭代器 iterator()
Iterator it = a.iterator();
// 返回 List 迭代器 listIterator()
ListIterator lit = a.listIterator();
// 转为数组
String [] aarray = a.toArray();
- 排序
public class RunoobTest {
public static void main(String[] args) {
ArrayList<String> sites = new ArrayList<String>();
sites.add("Taobao");
sites.add("Wiki");
sites.add("Runoob");
sites.add("Weibo");
sites.add("Google");
Collections.sort(sites); // 字母排序
for (String i : sites) {
System.out.println(i);
}
}
}
LinkedList
public class RunoobTest {
public static void main(String[] args) {
LinkedList<String> sites = new LinkedList<String>();
sites.add("Google");
sites.add("Runoob");
sites.add("Taobao");
// 使用 addFirst() 在头部添加元素
sites.addFirst("Wiki");
sites.addLast("Wiki");
System.out.println(sites);
}
}
//result: [Wiki, Google, Runoob, Taobao, Wiki]
其余容器可以查看
https://www.runoob.com/java/java-hashmap.html
Lecture12
异常处理
抛出异常
- 检查错误条件
- throw关键字
If (t == null) throw new NullPointerException();
处理异常
try {
// 可能会抛出异常的代码
}
catch (Type1Exception e){
// 处理类型为 "Type1Exception" 的异常
}
catch (Type2Exception e){
// 处理类型为 "Type2Exception" 的异常
}
catch (Type3Exception e){
// 处理类型为 "Type3Exception" 的异常
}
...
异常对象
- Exception的子类
- 含有toString(), printStackTrace()方法
- 通常不需要重写 Exception 类中的任何方法
class SimpleException extends Exception { }
public class InheritingExceptions {
public static void main(String[] args) {
try {
System.out.println("Throw SimpleException from f()");
throw new SimpleException();
} catch(SimpleException e) {
System.out.println("Caught it!");
System.out.println(e);
System.out.println(e.printStackTrace(System.out));
}
}
}
类方法的异常说明
- 标识该方法抛出什么样的异常
bar() throws Type1Exception, Type2Exception{
…
throw new Type1Exception ();
…
throw new Type2Exception ();
}
如果方法使用了 throws 关键字,则在该方法的调用处必须要处理相应的异常,否则编译不通过。
- 包含 throws 关键字 , 但函数本身并不抛出相应异常
-
- 用于 interface, abstract method
-
- 保证重写的方法必须考虑所列出的异常
- 不包含 throws 关键字,默认会抛出 RuntimeException 类型的异常
Java标准异常
- 都为 Exception 类的子类
- 通过名字知道种类
Lecture13
I/O
I/O流
- 数据的流向
-
- 外界数据进入程序
-
- 程序数据进入外界
- 数据的内容
-
- 字节
-
- 文件
-
- 字符串
-
- 对象
public static void fileOutput() throws IOException{
String str = "hello world!";
File file = new File("d:\\test2.txt"); // 创建文件
if(!file.exists()){
file.createNewFile(); // 如果文件不存在,则进行创建
}
FileOutputStream fOutput = new FileOutputStream(file);
BufferedOutputStream bOutput = new BufferedOutputStream(fOutput);
byte[] buffer = str.getBytes(); // 将字符串文本转换成字节数组
bOutput.write(buffer);
bOutput.close();
fOutput.close();
}
public static void fileInput() throws IOException{
File file = new File("d:\\test2.txt"); // 创建文件
FileInputStream finput = new FileInputStream(file);
BufferedInputStream bfinput = new BufferedInputStream(finput);
int temp = 0;
while((temp = bfinput.read())!= -1){ // 当 temp 为 -1 时,数据读取完毕
System.out.print((char)temp);
}
bfinput.close();
finput.close();
}
Path类型
- 创建path类型
Path p1 = Paths.get("C:/Document/tmp/Hello.java");
Path p2 = FileSystems.getDefault().getPath("C:/Document/tmp/Hello.java");
// Microsoft Windows syntax
Path path = Paths.get("C:\\home\\joe\\foo");
// Solaris syntax
// Path path = Paths.get("/home/joe/foo");
System.out.format("toString: %s%n", path.toString());
System.out.format("getFileName: %s%n", path.getFileName());
System.out.format("getName(0): %s%n", path.getName(0));
System.out.format("getNameCount: %d%n", path.getNameCount());
System.out.format("subpath(0,2): %s%n", path.subpath(0,2));
System.out.format("getParent: %s%n", path.getParent());
System.out.format("getRoot: %s%n", path.getRoot());
File类
参考https://www.runoob.com/java/java-files-io.html
Lecture 14
多线程编程
多线程的创建方式一:继承Thread类
- 定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法
- 创建MyThread类的对象
- 调用线程对象的start()方法启动线程(启动后还是执行run方法的)
优点:编码简单
缺点:线程类已经继承Thread,无法继承其他类,不利于功能的扩展。
多线程的创建方式二:实现Runnable接口
- 定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法
- 创建MyRunnable任务对象
- 把MyRunnable任务对象交给Thread处理。
- 调用线程对象的start()方法启动线程
优点:任务类只是实现接口,可以继续继承其他类、实现其他接口,扩展性强。
缺点:需要多一个Runnable对象。
多线程的第三种创建方式:利用Callable接口、FutureTask类来实现
- 创建任务对象
定义一个类实现Callable接口,重写call方法,封装要做的事情,和要返回的数据。
把Callable类型的对象封装成FutureTask(线程任务对象)。 - 调用Thread对象的start方法启动线程。
- 线程执行完毕后、通过FutureTask对象的的get方法去获取线程任务执行的结果。
优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强;可以在线程执行完毕后去获取线程执行的结果。
反射
反射指的是允许以编程方式访问已加载类的成分(成员变量、方法、构造器等)。
反射是在运行时获取类的字节码文件对象:然后可以解析类中的全部成分。
反射的核心思想和关键就是:得到编译以后的class文件对象。
反射作用:
- 可以在运行时得到一个类的全部成分然后操作
- 可以破坏封装性
- 也可以破坏泛型的约束性
- 更重要的用途是适合:做Java高级框架
- 基本上主流框架都会基于反射设计一些通用技术功能
设计原则
单一职责原则
一个对象应该只包含单一的职责,且该职责被完整地封装在一个类中。单一职责原则用于控制类的粒度大小。
开闭原则
软件实体应该对扩展开放,对修改关闭。
里氏替换原则
所有引用基类的地方必须能透明地使用其子类的对象。
子类可以扩展父类的功能,但不能改变父类原有的功能:
- 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法
- 子类可以增加自己特有的方法
- 当子类的方法重载父类的方法时,方法的前置条件要比父类的方法输入参数更宽松
- 当子类的方法实现父类的方法时,方法的输出、返回值比父类更严格或一样
依赖倒转原则
高层模块不应依赖于底层模块,它们都应该依赖抽象。抽象不应该依赖于细节,细节应该依赖于抽象。
接口隔离原则
客户端不应该依赖那些它不需要的接口。
接口隔离原则实际上是对接口的细化。
合成复用原则
优先使用对象组合,而不是用过继承打到复用的目的。
迪米特法则
迪米特法则(Law of Demeter)又称最少知识原则,是对程序内部数据交互的限制。
每一个软件单位对其他单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。
简单来说就是,一个类/模块对其他的类/模块有越少的交互越好。当一个类发生改动,那么,与其相关的类(比如用到此类啥方法的类)需要尽可能少的受影响(比如修改了方法名、字段名等,可能其他用到这些方法或是字段的类也需要跟着修改)这样我们在维护项目的时候会更加轻松一些。
设计模式
软件设计模式(Design pattern),又称设计模式,是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性、程序的重用性。
工厂方法模式
根据迪米特法则,我们应该尽可能地少与其他类进行交互,所以我们可以将那些需要频繁出现的对象创建,封装到一个工厂类中,当我们需要对象时,直接调用工厂类中的工厂方法来为我们生成对象,这样,就算类出现了变动,我们也只需要修改工厂中的代码即可,而不是大面积地进行修改。
抽象工厂模式
抽象工厂(AbstractFactory)模式是一种为访问类提供一个创建一组相关或相互依赖对象的接口,且访问类无须指定所要产品的具体类就能得到同族的不同等级的产品的模式结构。抽象工厂模式是工厂方法模式的升级版本,工厂方法模式只生产一个等级的产品,而抽象工厂模式可生产多个等级的产品。
使用抽象工厂模式一般要满足以下条件。
- 系统中有多个产品族,每个具体工厂创建同一族但属于不同等级结构的产品。
- 系统一次只可能消费其中某一族产品,即同族的产品一起使用。
建造者模式
相比直接去new一个新的对象,建造者模式的重心更加关注在如何完成每一步的配置,同时如果一个类的构造方法参数过多,我们通过建造者模式来创建这个对象,会更加优雅。
public class Student {
int id;
int age;
int grade;
String name;
String college;
String profession;
List<String> awards;
public Student(int id, int age, int grade, String name, String college, String profession, List<String> awards) {
this.id = id;
this.age = age;
this.grade = grade;
this.name = name;
this.college = college;
this.profession = profession;
this.awards = awards;
}
}
public static void main(String[] args) {
Student student = new Student(1, 18, 3, "小明", "计算机学院", "计算机科学与技术", Arrays.asList("ICPC-ACM 区域赛 金牌", "LPL 2022春季赛 冠军"));
}
更改为:
public class Student {
...
//一律使用建造者来创建,不对外直接开放
private Student(int id, int age, int grade, String name, String college, String profession, List<String> awards) {
...
}
public static StudentBuilder builder(){ //通过builder方法直接获取建造者
return new StudentBuilder();
}
public static class StudentBuilder{ //这里就直接创建一个内部类
//Builder也需要将所有的参数都进行暂时保存,所以Student怎么定义的这里就怎么定义
int id;
int age;
int grade;
String name;
String college;
String profession;
List<String> awards;
public StudentBuilder id(int id){ //直接调用建造者对应的方法,为对应的属性赋值
this.id = id;
return this; //为了支持链式调用,这里直接返回建造者本身,下同
}
public StudentBuilder age(int age){
this.age = age;
return this;
}
...
public StudentBuilder awards(String... awards){
this.awards = Arrays.asList(awards);
return this;
}
public Student build(){ //最后我们只需要调用建造者提供的build方法即可根据我们的配置返回一个对象
return new Student(id, age, grade, name, college, profession, awards);
}
}
}
public static void main(String[] args) {
Student student = Student.builder() //获取建造者
.id(1) //逐步配置各个参数
.age(18)
.grade(3)
.name("小明")
.awards("ICPC-ACM 区域赛 金牌", "LPL 2022春季赛 冠军")
.build(); //最后直接建造我们想要的对象
}
单例模式
在我们的整个程序中,同一个类始终只会有一个对象来进行操作。比如数据库连接类,实际上我们只需要创建一个对象或是直接使用静态方法就可以了,没必要去创建多个对象。
静态内部类实现:
public class Singleton {
private Singleton() {}
private static class Holder { //由静态内部类持有单例对象,但是根据类加载特性,我们仅使用Singleton类时,不会对静态内部类进行初始化
private final static Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance(){ //只有真正使用内部类时,才会进行类初始化
return Holder.INSTANCE; //直接获取内部类中的
}
}
原型模式
原型模式使用原型实例指定待创建对象的类型,并且通过复制这个原型来创建新的对象。
在Java中,我们就可以使用Cloneable接口提供的拷贝机制,来实现原型模式:
public class Student implements Cloneable{ //注意需要实现Cloneable接口
@Override
public Object clone() throws CloneNotSupportedException { //提升clone方法的访问权限
return super.clone();
}
}
类/对象适配器模式
Java中的适配器模式的核心思想是将一个类的接口转换成另一个类的接口,以便于这两个类能够协同工作。适配器模式通常用于以下情况:
- 当现有的类的接口与系统的需要不一致时,可以使用适配器模式将这些类的接口转换为所需的接口。
- 当需要重用一些现有的类,但其接口并不符合要求时,可以使用适配器模式对其进行包装,从而使其能够符合系统的需要。
- 当需要建立一个可以复用的类,其在与多个类协同工作时具有不同的接口要求时,可以使用适配器模式将这些不同的接口统一起来。
桥接模式
桥接模式是将抽象部分与它的实现部分分离,使它们都可以独立地变化。它是一种对象结构型模式,又称为柄体(Handle and Body)模式或接口(Interfce)模式。
桥梁模式所涉及的角色有:
- 抽象化(Abstraction)角色:抽象化给出的定义,并保存一个对实现化对象的引用。
- 修正抽象化(RefinedAbstraction)角色:扩展抽象化角色,改变和修正父类对抽象化的定义。
- 实现化(Implementor)角色:这个角色给出实现化角色的接口,但不给出具体的实现。必须指出的是,这个接口不一定和抽象化角色的接口定义相同,实际上,这两个接口可以非常不一样。实现化角色应当只给出底层操作,而抽象化角色应当只给出基于底层操作的更高一层的操作。
- 具体实现化(ConcreteImplementor)角色:这个角色给出实现化角色接口的具体实现。
将m*n个实现类转换为m+n个实现类。
组合模式
组合模式实际上就是将多个组件进行组合,让用户可以对它们进行一致性处理。比如我们的文件夹,一个文件夹中可以有很多个子文件夹或是文件。
装饰模式
装饰模式是一种结构型设计模式,允许你通过将对象放入包含行为的特殊封装对象中来为原对象绑定新的行为。
代理模式
代理模式是一种常见的设计模式,它允许你在不直接访问对象的情况下,通过一个代理对象来控制对该对象的访问。这个代理对象可以作为客户端和实际对象之间的中介,从而实现一些特定的控制功能,比如限制访问、记录访问日志等。
代理模式是一种结构型设计模式,它由三个主要角色组成:
抽象主题(Subject):定义了代理和实际对象之间的共同接口。这个接口通常包含了代理对象需要实现的方法。
实际主题(Real Subject):定义了代理对象所代表的实际对象。
代理(Proxy):代理对象实现了抽象主题中的接口,并保存了对实际主题的引用。代理对象可以控制对实际对象的访问,比如限制访问或记录访问日志等。
应用场景
代理模式可以在多种场景下使用,包括但不限于以下几个方面:
访问控制:代理模式可以用来控制对实际对象的访问权限。比如,只有特定用户或角色才能访问某些敏感数据。
远程访问:代理模式可以用来处理远程对象的访问。比如,通过代理对象来访问远程Web服务。
延迟加载:代理模式可以用来实现延迟加载。比如,通过代理对象来加载某些资源或数据,以避免在程序启动时就加载所有数据。
缓存:代理模式可以用来实现缓存。比如,代理对象可以缓存一些频繁访问的数据或结果,以提高程序的性能。
远程代理:当需要在不同的进程或机器之间进行通信时,可以使用远程代理来封装和管理通信的过程和数据。
虚拟代理:当需要延迟加载或预加载大量数据时,可以使用虚拟代理来提高程序的性能和效率。
安全代理:当需要限制外部对对象的访问权限时,可以使用安全代理来实现访问控制和管理。
缓存代理:当需要对经常使用的数据进行缓存时,可以使用缓存代理来管理和优化数据的访问效率。
智能指针代理:当需要对对象进行内存管理和安全释放时,可以使用智能指针代理来管理对象的生命周期。
外观模式
外观模式充分体现了迪米特法则。可能我们的整个项目有很多个子系统,但是我们可以在这些子系统的上面加一个门面(Facade)当我们外部需要与各个子系统交互时,无需再去直接使用各个子系统,而是与门面进行交互,再由门面与后面的各个子系统操作,这样,我们以后需要办什么事情,就统一找门面就行了。这样的好处是,首先肯定方便了代码的编写,统一找门面就行,不需要去详细了解子系统,并且,当子系统需要修改时,也只需要修改门面中的逻辑,不需要大面积的变动,遵循迪米特法则尽可能少的交互。
public class SubSystemA {
public void test1(){
System.out.println("排队");
}
}
public class SubSystemB {
public void test2(){
System.out.println("结婚");
}
}
public class SubSystemC {
public void test3(){
System.out.println("领证");
}
}
public class Facade {
SubSystemA a = new SubSystemA();
SubSystemB b = new SubSystemB();
SubSystemC c = new SubSystemC();
public void marry(){ //红白喜事一条龙服务
a.test1();
b.test2();
c.test3();
}
}
这样就用一个类完成了所有流程。
享元模式
在某些情况下,一个应用程序可能需要创建大量相似的对象,这些对象之间的差异仅在于一些内部状态。如果为每个对象都分配独立的内存,会导致内存占用量急剧增加,降低系统的性能和效率。享元模式通过共享相同的对象实例,来减少对内存的需求。
享元模式的核心思想是将对象的状态分为内部状态和外部状态。内部状态是对象的固定部分,可以在多个对象之间共享,而外部状态是对象的变化部分,需要在使用时传递给对象。享元模式将内部状态存储在享元对象中,并将外部状态作为参数传递给方法。
享元模式包含以下几个关键组件:
- 享元接口:定义了享元对象的通用接口,通过该接口可以获取内部状态和操作享元对象。
- 具体享元:实现了享元接口,并包含内部状态。具体享元对象需要是可共享的,也就是说它们可以在多个上下文中共享。
- 享元工厂:负责创建和管理享元对象。它维护一个享元池,用于存储已经创建的享元对象,以便在需要时进行复用。
当客户端需要使用享元对象时,可以通过享元工厂获取对象的实例。如果享元池中已经存在相应的对象实例,则直接返回该实例;如果不存在,则创建一个新的享元对象并添加到享元池中。这样可以确保相同的对象在多个地方共享使用,减少内存消耗。
解释器模式
解释器顾名思义,就是对我们的语言进行解释,根据不同的语义来做不同的事情,比如我们在SE中学习的双栈计算器,正是根据我们输入的算式,去进行解析,并根据不同的运算符来不断进行计算。
模板方法模式
某些操作是固定的,我们就可以直接在类中对应方法进行编写,但是可能某些操作需要视情况而定,由不同的子类实现来决定,这时,我们就需要让这些操作由子类来延迟实现了。现在我们就需要用到模板方法模式。
责任链模式
责任链模式是一种设计模式。在责任链模式里,很多对象由每一个对象对其下家的引用而连接起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求。发出这个请求的客户端并不知道链上的哪一个对象最终处理这个请求,这使得系统可以在不影响客户端的情况下动态地重新组织和分配责任。
命令模式
我们日常的开发就很好地体现出了命令模式,比如:领导将开发任务指派给程序员去完成。其中领导就是“命令的发布者(调用者)”,程序员就是“命令的具体执行者(接收者)”,这个指派的动作就是“具体的命令”。命令将调用者和接收者进行连接,从而完成开发任务,这一套流程就可以看作是命令模式的执行原理。
命令模式使得请求的发送者与请求的执行者之间消除耦合,让对象之间的调用关系更加灵活。在命令模式中,会将一个命令封装成一个对象,同时命令模式也支持可撤销的操作。
迭代器模式
在现实生活以及程序设计中,经常要访问一个聚合对象中的各个元素,如“数据结构”中的链表遍历,通常的做法是将链表的创建和遍历都放在同一个类中,但这种方式不利于程序的扩展,如果要更换遍历方法就必须修改程序源代码,这违背了 “开闭原则”。
“迭代器模式”能较好地克服以上缺点,它在客户访问类与聚合类之间插入一个迭代器,这分离了聚合对象与其遍历行为,对客户也隐藏了其内部细节,且满足“单一职责原则”和“开闭原则”,如 Java 中的 Collection、List、Set、Map 等都包含了迭代器。
中介者模式
在现实生活中,常常会出现好多对象之间存在复杂的交互关系,这种交互关系常常是“网状结构”,它要求每个对象都必须知道它需要交互的对象。例如,每个人必须记住他(她)所有朋友的电话;而且,朋友中如果有人的电话修改了,他(她)必须让其他所有的朋友一起修改,这叫作“牵一发而动全身”,非常复杂。
如果把这种“网状结构”改为“星形结构”的话,将大大降低它们之间的“耦合性”,这时只要找一个“中介者”就可以了。如前面所说的“每个人必须记住所有朋友电话”的问题,只要在网上建立一个每个朋友都可以访问的“通信录”就解决了。这样的例子还有很多,例如,你刚刚参加工作想租房,可以找“房屋中介”;或者,自己刚刚到一个陌生城市找工作,可以找“人才交流中心”帮忙。
在软件的开发过程中,这样的例子也很多,例如,在 MVC 框架中,控制器(C)就是模型(M)和视图(V)的中介者;还有大家常用的 QQ 聊天程序的“中介者”是 QQ 服务器。所有这些,都可以采用“中介者模式”来实现,它将大大降低对象之间的耦合性,提高系统的灵活性。
备忘录模式
备忘录模式通过引入“备忘录”对象,允许在不暴露对象内部结构的情况下,捕获并存储对象的状态。同时,它还提供了一种将对象恢复到之前状态的方式。
观察者模式
在Java中,一个对象的状态发生改变,可能就会影响到其他的对象,与之相关的对象可能也会联动的进行改变。还有我们之前遇到过的监听器机制,当具体的事件触发时,我们在一开始创建的监听器就可以执行相关的逻辑。我们可以使用观察者模式来实现这样的功能,当对象发生改变时,观察者能够立即观察到并进行一些联动操作。
状态模式
对象可能存在很多种状态,在不同的状态下会有不同的行为,我们就可以通过状态模式来实现。
策略模式
我们可以为对象设定一种策略,这样对象之后的行为就会按照我们在一开始指定的策略而决定了,看起来和前面的状态模式很像,但是,它与状态模式的区别在于,这种转换是“主动”的,是由我们去指定,而状态模式,可能是在运行过程中自动切换的。比如线程池的拒绝策略。
访问者模式
访问者模式是一种行为型设计模式,它可以将算法与对象结构分离开来,使得算法可以独立于对象来变化。该模式主要解决的问题是在不修改对象自身的基础上,对对象的结构进行增删改等操作。
在访问者模式中,对象结构包含多个具体元素,每个具体元素都可以接受一个访问者进行访问,而访问者可以对不同的具体元素进行不同的操作。
在此过程中,访问者可以通过访问具体元素所提供的接口来访问和操作具体元素。