一、沉默王二-面向对象编程
1.this与super关键字
1)this
this 关键字有很多种用法,其中最常用的一个是,它可以作为引用变量,指向当前对象。
this()可以调用当前类的构造方法;- this 可以作为参数在方法中传递;
- this 可以作为参数在构造方法中传递;
- this 可以作为方法的返回值,返回当前类的对象。
public class WithoutThisStudent {
String name;
int age;
WithoutThisStudent(String name, int age) {
name = name;
age = age;
}
void out() {
System.out.println(name+" " + age);
}
public static void main(String[] args) {
WithoutThisStudent s1 = new WithoutThisStudent("沉默王二", 18);
WithoutThisStudent s2 = new WithoutThisStudent("沉默王三", 16);
s1.out();
s2.out();
//输出为:null 0
// null 0
}
}
尽管创建对象的时候传递了参数,但实例变量并没有赋值。这是因为如果构造方法中没有使用 this 关键字的话,name 和 age 指向的并不是实例变量而是参数本身。
public class ThisAsConstrutorParam {
int count = 10;
ThisAsConstrutorParam() {
Data data = new Data(this);
data.out();
}
public static void main(String[] args) {
new ThisAsConstrutorParam();
}
}
class Data {
ThisAsConstrutorParam param;
Data(ThisAsConstrutorParam param) {
this.param = param;
}
void out() {
System.out.println(param.count);
}
}
“在构造方法 ThisAsConstrutorParam() 中,我们使用 this 关键字作为参数传递给了 Data 对象,它其实指向的就是 new ThisAsConstrutorParam() 这个对象。”
“this 关键字也可以作为参数在构造方法中传递,它指向的是当前类的对象。当我们需要在多个类中使用一个对象的时候,这非常有用。”输出结果为:10。
2)super
super 关键字的用法主要有三种。
- 指向父类对象;
- 调用父类的方法;
super()可以调用父类的构造方法。
其实和 this 有些相似,只不过用意不大相同。
每当创建一个子类对象的时候,也会隐式的创建父类对象,由 super 关键字引用。
如果父类和子类拥有同样名称的字段,super 关键字可以用来访问父类的同名字段。
public class ReferParentField {
public static void main(String[] args) {
new Dog().printColor();
}
}
class Animal {
String color = "白色";
}
class Dog extends Animal {
String color = "黑色";
void printColor() {
System.out.println(color);
System.out.println(super.color);
}
}
输出为:黑色 白色。
“super() 也可以用来调用父类的有参构造方法,这样可以提高代码的可重用性。”
class Person {
int id;
String name;
Person(int id, String name) {
this.id = id;
this.name = name;
}
}
class Emp extends Person {
float salary;
Emp(int id, String name, float salary) {
super(id, name);
this.salary = salary;
}
void display() {
System.out.println(id + " " + name + " " + salary);
}
}
public class CallParentParamConstrutor {
public static void main(String[] args) {
new Emp(1, "沉默王二", 20000f).display();
}
}
输出为:1 沉默王二 20000.0
“Emp 类继承了 Person 类,也就继承了 id 和 name 字段,当在 Emp 中新增了 salary 字段后,构造方法中就可以使用 super(id, name) 来调用父类的有参构造方法。”
2.static关键字
1)static变量
static 关键字的作用可以用一句话来描述:‘方便在没有创建对象的情况下进行调用,包括变量和方法’。也就是说,只要类被加载了,就可以通过类名进行访问。”我扶了扶沉重眼镜,继续说到,“static 可以用来修饰类的成员变量,以及成员方法。我们一个个来看。”
如果在声明变量的时候使用了 static 关键字,那么这个变量就被称为静态变量。静态变量只在类加载的时候获取一次内存空间,这使得静态变量很节省内存空间。
public class Student {
String name;
int age;
static String school = "郑州大学";
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public static void main(String[] args) {
Student s1 = new Student("沉默王二", 18);
Student s2 = new Student("沉默王三", 16);
}
}
s1 和 s2 这两个引用变量存放在栈区(stack),沉默王二+18 这个对象和沉默王三+16 这个对象存放在堆区(heap),school 这个静态变量存放在静态区。
public class Counter {
int count = 0;
Counter() {
count++;
System.out.println(count);
}
public static void main(String args[]) {
Counter c1 = new Counter();
Counter c2 = new Counter();
Counter c3 = new Counter();
}
}
输出为1,1,1。
我们创建一个成员变量 count,并且在构造函数中让它自增。因为成员变量会在创建对象的时候获取内存,因此每一个对象都会有一个 count 的副本, count 的值并不会随着对象的增多而递增。
如果改为static int count=0 则会输出:1,2,3。
由于静态变量只会获取一次内存空间,所以任何对象对它的修改都会得到保留,所以每创建一个对象,count 的值就会加 1,所以最终的结果是 3。这就是静态变量和成员变量之间的差别。
2)static方法
“静态方法有以下这些特征。”
- 静态方法属于这个类而不是这个类的对象;
- 调用静态方法的时候不需要创建这个类的对象;
- 静态方法可以访问静态变量。
public class StaticMethodStudent {
String name;
int age;
static String school = "郑州大学";
public StaticMethodStudent(String name, int age) {
this.name = name;
this.age = age;
}
static void change() {
school = "河南大学";
}
void out() {
System.out.println(name + " " + age + " " + school);
}
public static void main(String[] args) {
StaticMethodStudent.change();
StaticMethodStudent s1 = new StaticMethodStudent("沉默王二", 18);
StaticMethodStudent s2 = new StaticMethodStudent("沉默王三", 16);
s1.out();
s2.out();
}
}
输出为:沉默王二 18 河南大学 沉默王三 16 河南大学
“change() 方法就是一个静态方法,所以它可以直接访问静态变量 school,把它的值更改为河南大学;并且,可以通过类名直接调用 change() 方法,就像 StaticMethodStudent.change() 这样。”
需要注意的是,静态方法不能访问非静态变量和调用非静态方法。
为什么 main 方法是静态的?
“如果 main 方法不是静态的,就意味着 Java 虚拟机在执行的时候需要先创建一个对象才能调用 main 方法,而 main 方法作为程序的入口,创建一个额外的对象显得非常多余。”
“java.lang.Math 类的几乎所有方法都是静态的,可以直接通过类名来调用,不需要创建类的对象。”
3)static代码块
public class StaticBlock {
static {
System.out.println("静态代码块");
}
public static void main(String[] args) {
System.out.println("main 方法");
}
}
“静态代码块通常用来初始化一些静态变量,它会优先于 main() 方法执行。”
4)static内部类
public class Singleton {
private Singleton() {}
private static class SingletonHolder {
public static final Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
}
第一次加载 Singleton 类时并不会初始化 instance,只有第一次调用 getInstance() 方法时 Java 虚拟机才开始加载 SingletonHolder 并初始化 instance,这样不仅能确保线程安全,也能保证 Singleton 类的唯一性。不过,创建单例更优雅的一种方式是使用枚举。
需要注意的是。第一,静态内部类不能访问外部类的所有成员变量;第二,静态内部类可以访问外部类的所有静态变量,包括私有静态变量。第三,外部类不能声明为 static。
3.final关键字
1)final变量
被 final 修饰的变量无法重新赋值。换句话说,final 变量一旦初始化,就无法更改。
final 和 static 一起修饰的成员变量叫做常量,常量名必须全部大写。
“被 final 修饰的方法不能被重写。如果我们在设计一个类的时候,认为某些方法不应该被重写,就应该把它设计成 final 的。”
2)final方法
“Thread 类就是一个例子,它本身不是 final 的,这意味着我们可以扩展它,但它的 isAlive() 方法是 final 的。”
“一个类是 final 的,和一个类不是 final,但它所有的方法都是 final 的,考虑一下,它们之间有什么区别?”
“我能想到的一点,就是前者不能被继承,也就是说方法无法被重写;后者呢,可以被继承,然后追加一些非 final 的方法。
二、小博哥-java设计模式
1.责任链模式
责任链模式的核心是解决一组服务中的先后执行处理关系,就有点像你没钱花了,需要家庭财务支出审批,10块钱以下找闺女审批,100块钱先闺女审批再媳妇审批。你可以理解想象成当你要跳槽的时候被安排的明明白白的被各个领导签字放行。
优化前:
itstack-demo-design-13-01
└── src
└── main
└── java
└── org.itstack.demo.design
└── AuthController.java
优化后:
itstack-demo-design-13-02
└── src
└── main
└── java
└── org.itstack.demo.design
├── impl
│ ├── Level1AuthLink.java
│ ├── Level2AuthLink.java
│ └── Level3AuthLink.java
├── AuthInfo.java
└── AuthLink.java
- 上图是这个业务模型中责任链结构的核心部分,通过三个实现了统一抽象类
AuthLink的不同规则,再进行责任编排模拟出一条链路。这个链路就是业务中的责任链。 - 一般在使用责任链时候如果是场景比较固定,可以通过写死到代码中进行初始化。但如果业务场景经常变化可以做成xml配置的方式进行处理,也可以落到库里进行初始化操作。
- 从上面代码从if语句重构到使用责任链模式开发可以看到,我们的代码结构变得清晰干净了,也解决了大量if语句的使用。并不是if语句不好,只不过if语句并不适合做系统流程设计,但是在做判断和行为逻辑处理中还是非常可以使用的。
- 在我们前面学习结构性模式中讲到过组合模式,它像是一颗组合树一样,我们搭建出一个流程决策树。其实这样的模式也是可以和责任链模型进行组合扩展使用,而这部分的重点在于如何关联链路的关联,最终的执行都是在执行在中间的关系链。
2.命令模式
命令模式是行为模式中的一种,以数据驱动的方式将命令对象,可以使用构造函数的方式传递给调用者。调用者再提供相应的实现为命令执行提供操作方法。可能会感觉这部分有一些饶,可以通过对代码的实现进行理解,再通过实操来熟练。
在这个设计模式的实现过程中有如下几个比较重要的点:
- 抽象命令类:声明执行命令的接口和方法
- 具体的命令实现类:接口类的具体实现,可以是一组相似的行为逻辑
- 实现者:也就是为命令做实现的具体实现类
- 调用者:处理命令、实现的具体操作者,负责对外提供命令服务
优化前:
itstack-demo-design-14-01
└── src
└── main
└── java
└── org.itstack.demo.design
└── XiaoEr.java
优化后:
itstack-demo-design-14-02
└── src
├── main
│ └── java
│ └── org.itstack.demo.design
│ ├── cook
│ │ ├── impl
│ │ │ ├── GuangDongCook.java
│ │ │ ├── JiangSuCook.java
│ │ │ ├── ShanDongCook.java
│ │ │ └── SiChuanCook.java
│ │ └── ICook.java
│ ├── cuisine
│ │ ├── impl
│ │ │ ├── GuangDoneCuisine.java
│ │ │ ├── JiangSuCuisine.java
│ │ │ ├── ShanDongCuisine.java
│ │ │ └── SiChuanCuisine.java
│ │ └── ICuisine.java
│ └── XiaoEr.java
└── test
└── java
└── org.itstack.demo.test
└── ApiTest.java
- 从以上的内容和例子可以感受到,命令模式的使用场景需要分为三个比较大的块:
命令、实现、调用者,而这三块内容的拆分也是选择适合场景的关键因素,经过这样的拆分可以让逻辑具备单一职责的性质,便于扩展。 - 通过这样的实现方式与if语句相比,降低了耦合性也方便其他的命令和实现的扩展。但同时这样的设计模式也带来了一点问题,就是在各种命令与实现的组合下,会扩展出很多的实现类,需要进行管理。
3.迭代器模式
迭代器模式,常见的就是我们日常使用的iterator遍历。虽然这个设计模式在我们的实际业务开发中的场景并不多,但却几乎每天都要使用jdk为我们提供的list集合遍历。另外增强的for循环虽然是循环输出数据,但是它不是迭代器模式。迭代器模式的特点是实现Iterable接口,通过next的方式获取集合元素,同时具备对元素的删除等操作。而增强的for循环是不可以的。
这种设计模式的优点是可以让我们以相同的方式,遍历不同的数据结构元素,这些数据结构包括:数组、链表、树等,而用户在使用遍历的时候并不需要去关心每一种数据结构的遍历处理逻辑,从让使用变得统一易用。
优化后:
itstack-demo-design-15-00
└── src
├── main
│ └── java
│ └── org.itstack.demo.design
│ ├── group
│ │ ├── Employee.java
│ │ ├── GroupStructure.java
│ │ └── Link.java
│ └── lang
│ ├── Collection.java
│ ├── Iterable.java
│ └── Iterator.java
└── test
└── java
└── org.itstack.demo.design.test
└── ApiTest.java
-
以上是我们工程类图的模型结构,左侧是对迭代器的定义,右侧是在数据结构中实现迭代器功能。
-
关于左侧部分的实现与jdk中的方式是一样的,所以在学习的过程中可以互相参考,也可以自己扩展学习。
-
另外这个遍历方式一个树形结构的深度遍历,为了可以更加让学习的小伙伴容易理解,这里我实现了一种比较简单的树形结构深度遍历方式。后续读者也可以把遍历扩展为横向遍历也就是宽度遍历。
-
迭代器的设计模式从以上的功能实现可以看到,满足了单一职责和开闭原则,外界的调用方也不需要知道任何一个不同的数据结构在使用上的遍历差异。可以非常方便的扩展,也让整个遍历变得更加干净整洁。
-
但从结构的实现上可以看到,迭代器模式的实现过程相对来说是比较复杂的,类的实现上也扩增了需要外部定义的类,使得遍历与原数据结构分开。虽然这是比较麻烦的,但可以看到在使用java的jdk时候,迭代器的模式还是很好用的,可以非常方便扩展和升级。