JDK JRE JVM
JDK
Java Development Kit 用作开发, 包含了 JRE, 编译器和其他的工具(比如: JavaDoc,Java 调试器), 可以让开发者开发、编译、执行 Java 应用程序.
JRE
Java 运行时环境是将要执行 Java 程序的 Java 虚拟机, (运行 Java 程序的用户使用的软件)可以想象成它是一个容器, JVM 是它的内容.
JRE = JVM + Java Packages Classes(like util, math, lang, awt,swing etc)+runtime libraries.
JVM
Java virtual machine (Java 虚拟机) 是一个可以执行 Java 编译产生的 Java class 文件 (bytecode) 的虚拟机进程, 是一个纯的运行环境.
面向对象
面向对象 与 面向过程的区别
面向过程:是分析解决问题的步骤,然后用函数把这些步骤一步一步地实现,然后在使用的时候一一调用则可。性能较高,所以单片机、嵌入式开发等一般采用面向过程开发
面向对象:是把构成问题的事务分解成各个对象,而建立对象的目的也不是为了完成一个个步骤,而是为了描述某个事物在解决整个问题的过程中所发生的行为。面向对象有封装、继承、多态的特性,所以易维护、易复用、易扩展。可以设计出低耦合的系统。 但是性能上来说,比面向过程要低。
四大特性
封装:一个对象他所封装的是自己的属性和方法,所以它是不需要依赖其他对象就可以完成自己的操作,封装就是把一个对象的属性私有化,同时提供一些可以被外界访问属性的方法
继承:继承的基本思想是,可以基于已有的类创建新的类。继承已存在的类就是复用(继承)这些类的方法,而且可以增加一些新的方法和字段,使得新类能够适应新的情况。
多态:多态性是指允许不同类的对象对同一消息作出响应。多态性包括参数化多态性和包含多态性。多态性语言具有灵活、抽象、行为共享、代码共享的优势,很好的解决了应用程序函数同名问题。
编译时多态:方法重载实现
运行时多态:方法重写实现
抽象:声明方法的存在而不去实现它的类被叫做抽象类
封装
由于我们程序设计追求”高内聚,低耦合”;
其中高内聚指的是类的内部数据操作细节自己完成,不允许外部干涉;低耦合:仅暴露少量的方法给外部使用;
而封装,就是对数据的隐藏
其实在后期开发中不难发现,封装往往就是体现在定义一个 pojo 类,其中的属性私有,设置相应的 getter/setter
1 | public class Student { |
继承
- 继承的本质是对某一批类的抽象,从而实现对现实世界更好的建模
- extends 关键字是 扩展的意思,也就是子类是父类的扩展
- Java 中类只有单继承,没有多继承,因此必须慎重考虑继承的类;比如在后期我们实现多线程可以有继承和实现接口的方法,通常使用实现接口去实现,而不选择用继承;
- 继承是类与类的一种关系,除此之外,类与类之间关系还有==依赖、组合、聚合==等
- “is - a”关系是继承的一个明显特征
- Java 中,所有的类,都默认直接或者间接继承 Object 类
super
- 私有的东西是无法被子类继承的
super 需要注意的点:
super 调用父类的构造方法,必须在构造方法的第一行
super 必须只能出现在子类的方法 或 构造方法中,意思就是 如果直接在子类中直接写个 super 什么用都没 会报错
super 和 this 不能同时调用构造方法
super 和 this 区别:
- 代表的对象不同
- this:代表本身调用者这个对象
- super:代表父类对象的引用
- 使用的前提不同
- this:就算不用继承也能使用
- super:只能在继承条件下才能使用
- 调用构造方法不同
- this:调用本类的构造方法
- super:调用父类的构造
方法重写
重写与重载是不同的;
重写(Override)是父类与子类之间的多态性,实质是对父类的函数进行重新定义,如果在子类中定义某方法与其父类有相同的名称和参数则该方法被重写,不过子类函数的访问修饰权限不能小于父类的;若子类中的方法与父类中的某一方法具有相同的方法名、返回类型和参数表,则新方法将覆盖原有的方法,如需父类中原有的方法则可使用==super==关键字。
重载(Overload)是让类以统一的方式处理不同类型数据的一种手段,实质表现就是多个具有不同的参数个数或者类型的同名函数
(返回值类型可随意,不能以返回类型作为重载函数的区分标准)同时存在于同一个类中,是一个类中多态性的一种表现(调用方法时通过传递不同参数个数和参数类型
来决定具体使用哪个方法的多态性)。
重写需要满足的条件
需要是继承的关系,没有继承根本没有重写一说
方法名字必须相同
参数列表必须相同
子类返回值必须 小于等于 父类方法返回值
子类的权限修饰符必须 大宇等于 父类方法的权限修饰符
其中:public > protected > default(也就是什么都不写) > private
抛出的异常 只能被缩小,不能被扩大
多态
多态:即同一方法可以根据发送对象的不同而采用多种不同的行为方式
一个对象的实际类型其实是确定的,但是指向对象的引用类型有很多(仔仔细细理解这句话的意思)
其实这句话是什么意思呢?
1
2
3new A();
new B();
//这个叫对象的实际类型,根据new xxx来的 new出来是什么就其实确定了1
2
3A a
B a
//这个叫指向对象的引用类型 它就不是一定的了 可以实现父类的引用 指向 子类的类型
关于多态 经常听到这么一句话:
==成员变量、静态方法==:编译和运行看左边;==非静态方法==:编译看左边、运行看右边;
那到底是什么意思呢?
1 | public class B { |
1 | public class A extends B { |
1 | public class Main1 { |
运行结果:
不难发现:如果是==static==修饰的方法以及成员对象,我们看对象左边的类型就好,和右边没什么关系;
而不用==static==修饰的方法,我们就看右边的类型就行,和左边没什么关系
我们学习不能知其然而不知其所以然,我们来探究下为什么会这样呢?
B b = new A();
这行代码到底代表着什么呢?
B b;这里声明了一个变量 b 是属于 B 这个类的
= new A(); 建立了一个 A 的对象,赋值给了 b 那这又代表什么呢?
我们现在获得了一个被 A 类函数覆盖后的 B 类对象 b
那么问题转移了,我们只需要明白类所拥有的函数和变量到底是怎么加载的就好,就能明白为什么运行出来是这个结果了。
由于我们只是被覆盖了方法(非静态),成员变量还是那个成员变量,因此输出出来是各自的成员变量
那么静态方法呢?为什么还是各自的,没有被覆盖掉呢?
因为==static==修饰的函数,跟随 B 类的加载而加载,也就是 B 类的函数先于对象建立之前就存在,无法再被覆盖了。
还有哪些关键词修饰的方法是不能被覆盖的呢?
- static
- final
- private
instanceof 和 类型转换
- instanceof 是一个二元运算符 类似于 == > <等操作符,也是 Java 的一个关键字
- 其作用是测试它左边的对象是否是其右边的类的实例,然后返回 boolean 的数据类型(true of false)
==obj 必须为引用类型,不能是基本类型==
1
2
3int i = 0;
System.out.println(i instanceof Integer);//编译不通过
System.out.println(i instanceof Object);//编译不通过==obj 为 null==
1
System.out.println(null instanceof Object);//false
关于 null 类型的描述在官方文档:https://docs.oracle.com/javase/specs/jls/se7/html/jls-4.html#jls-4.1 有一些介绍。
一般我们知道 Java 分为两种数据类型,一种是基本数据类型,有八个分别是 byte short int long float double char boolean,一种是引用类型,包括类,接口,数组等等。
而 Java 中还有一种特殊的 null 类型,该类型没有名字,所以不可能声明为 null 类型的变量或者转换为 null 类型,null 引用是 null 类型表达式唯一可能的值,null 引用也可以转换为任意引用类型。
我们不需要对 null 类型有多深刻的了解,我们只需要知道 null 是可以成为任意引用类型的特殊符号。
在 JavaSE 规范 中对 instanceof 运算符的规定就是:如果 obj 为 null,那么将返回 false。
==obj 为 class 类的实例对象==
1
2Integer integer = new Integer(1);
System.out.println(integer instanceof Integer);//true==obj 为 class 接口的实现类==
了解 Java 集合的,我们知道集合中有个上层接口 List,其有个典型实现类 ArrayList
1
2public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable所以我们可以用 instanceof 运算符判断 某个对象是否是 List 接口的实现类,如果是返回 true,否则返回 false
1
2ArrayList arrayList = new ArrayList();
System.out.println(arrayList instanceof List);//true或者反过来也是返回 true
1
2List list = new ArrayList();
System.out.println(list instanceof ArrayList);//true==obj 为 class 的直接或间接子类==
我们新建一个父类 Person.class,然后在创建它的一个子类 Man.class
1
2
3public class Person {
}Man.class
1
2
3public class Man extends Person{
}测试:
1
2
3
4
5
6Person p1 = new Person();
Person p2 = new Man();
Man m1 = new Man();
System.out.println(p1 instanceof Man);//false
System.out.println(p2 instanceof Man);//true
System.out.println(m1 instanceof Man);//true注意第一种情况, p1 instanceof Man ,Man 是 Person 的子类,Person 不是 Man 的子类,所以返回结果为 false。
==问题==
前面我们说过编译器会检查 obj 是否能转换成右边的 class 类型,如果不能转换则直接报错,如果不能确定类型,则通过编译,具体看运行时定。
看如下几个例子:
1
2
3
4
5
6Person p1 = new Person();
System.out.println(p1 instanceof String);//编译报错
System.out.println(p1 instanceof List);//false
System.out.println(p1 instanceof List<?>);//false
System.out.println(p1 instanceof List<Person>);//编译报错按照我们上面的说法,这里就存在问题了,Person 的对象 p1 很明显不能转换为 String 对象,那么自然 Person 的对象 p1 instanceof String 不能通过编译,但为什么 p1 instanceof List 却能通过编译呢?而 instanceof List
又不能通过编译了? 根据 java SE 8:
代码:
1
2
3
4
5
6
7
8
9
10
11boolean result;
if (obj == null) {
result = false;
} else {
try {
T temp = (T) obj; // checkcast
result = true;
} catch (ClassCastException e) {
result = false;
}
}也就是说有表达式 obj instanceof T,instanceof 运算符的 obj 操作数的类型必须是引用类型或空类型; 否则,会发生编译时错误。
如果 obj 强制转换为 T 时发生编译错误,则关系表达式的 instanceof 同样会产生编译时错误。 在这种情况下,表达式实例的结果永远为 false。
在运行时,如果 T 的值不为 null,并且 obj 可以转换为 T 而不引发 ClassCastException,则 instanceof 运算符的结果为 true。 否则结果是错误的
简单来说就是:如果 obj 不为 null 并且 (T) obj 不抛 ClassCastException 异常则该表达式值为 true ,否则值为 false 。
所以对于上面提出的问题就很好理解了,为什么 p1 instanceof String 编译报错,因为(String)p1 是不能通过编译的,而 (List)p1 可以通过编译。
抽象
- abstract 修饰符可以用来修饰方法,也可以修饰类,如果修饰方法。
七种设计原则
面向对象七大设计原则:
1、 ==开闭原则(OCP:Open Closed Principle)==
核心:对扩展开放,对修改关闭。即在设计一个模块的时候,应当使这个模块可以在不被修改的前提下被扩展。
- 根据开闭原则,在设计一个软件系统模块(类,方法)的时候,应该可以在不修改原有的模块(修改关闭)的基础上,能扩展其功能(扩展开放)。
2、 ==里氏替换原则(LSP:Liskov Substitution Principle)==
核心:在任何父类出现的地方都可以用他的子类来替代(子类应当可以替换父类并出现在父类能够出现的任何地方)
- 1.子类必须完全实现父类的方法。在类中调用其他类是务必要使用父类或接口,如果不能使用父类或接口,则说明类的设计已经违背了 LSP 原则。
- 2.子类可以有自己的个性。子类当然可以有自己的行为和外观了,也就是方法和属性
- 3.覆盖或实现父类的方法时输入参数可以被放大。即子类可以重载父类的方法,但输入参数应比父类方法中的大,这样在子类代替父类的时候,调用的仍然是父类的方法。即以子类中方法的前置条件必须与超类中被覆盖的方法的前置条件相同或者更宽松。
- 4.覆盖或实现父类的方法时输出结果可以被缩小。
3、 ==单一职责原则(SRP:Single responsibility principle)==
核心:解耦和增强内聚性(高内聚,低耦合)
- 类被修改的几率很大,因此应该专注于单一的功能。如果你把多个功能放在同一个类中,功能之间就形成了关联,改变其中一个功能,有可能中止另一个功能,这时就需要新一轮的测试来避免可能出现的问题。
4、==接口隔离原则(ISP:Interface Segregation Principle)==
核心思想:不应该强迫客户程序依赖他们不需要使用的方法。接口分离原则的意思就是:一个接口不需要提供太多的行为,一个接口应该只提供一种对外的功能,不应该把所有的操作都封装到一个接口当中.
- 分离接口的两种实现方法:
- 1.使用委托分离接口。(Separation through Delegation)
- 2.使用多重继承分离接口。(Separation through Multiple Inheritance)
5、==依赖倒置原则(DIP:Dependence Inversion Principle)==
核心:要依赖于抽象,不要依赖于具体的实现
- 1.高层模块不应该依赖低层模块,两者都应该依赖其抽象(抽象类或接口)
- 2.抽象不应该依赖细节(具体实现)
- 3.细节(具体实现)应该依赖抽象。
三种实现方式: - 1.通过构造函数传递依赖对象
- 2.通过 setter 方法传递依赖对象
- 3.接口声明实现依赖对象
6、 ==迪米特原则(最少知识原则)(LOD:Law of Demeter)==
核心思想:一个对象应当对其他对象有尽可能少的了解,不和陌生人说话。(类间解耦,低耦合)意思就是降低各个对象之间的耦合,提高系统的可维护性;在模块之间只通过接口来通信,而不理会模块的内部工作原理,可以使各个模块的耦合成都降到最低,促进软件的复用
注:
- 1.在类的划分上,应该创建有弱耦合的类;
- 2.在类的结构设计上,每一个类都应当尽量降低成员的访问权限;
- 3.在类的设计上,只要有可能,一个类应当设计成不变;
- 4.在对其他类的引用上,一个对象对其它对象的引用应当降到最低;
- 5.尽量降低类的访问权限;
- 6.谨慎使用序列化功能;
- 7.不要暴露类成员,而应该提供相应的访问器(属性)
7、 ==组合/聚合复用原则(CRP:Composite Reuse Principle)==
核心思想:尽量使用对象组合,而不是继承来达到复用的目的。该原则就是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分:新的对象通过向这些对象的委派达到复用已有功能的目的。
复用的种类:
- 1.继承
- 2.合成聚合
注:在复用时应优先考虑使用合成聚合而不是继承
数据类型
四大数据类型
Java 是一种强类型的语言,意味着必须给每一个变量声名一种类型
Java 中一共有着八种基本类型
包含着 4 种整型(int short long byte)
两种浮点型(float double)
一种字符类型(char)
一种表示真值的boolean类型
整型
序号 | 数据类型 | 大小/位 | 封装类 | 默认值 | 可表示数据范围 | 存储需求 |
---|---|---|---|---|---|---|
1 | byte(位) | 8 | Byte | 0 | -128~127 | 1 字节 |
2 | short(短整数) | 16 | Short | 0 | -32768~32767 | 2 字节 |
3 | int(整数) | 32 | Integer | 0 | -2147483648~2147483647 | 4 字节 |
4 | long(长整数) | 64 | Long | 0 | -9223372036854775808~9223372036854775807 | 8 字节 |
int 与 Integer 的区别
Integer 是 int 的封装类,是引用类型。int 默认值是 0,而 Integer 默认值是 null,所以 Integer 能区分出 0 和 null 的情况。一旦 java 看到 null,就知道这个引用还没有指向某个对象
short s1 = 1; s1 = s1 + 1;有错吗?short s1 = 1; s1 += 1;有错吗?
对于short s1 = 1; s1 = s1 + 1;由于 1 是 int 类型,因此 s1+1 运算结果也是 int 型,需要强制转换类型才能赋值给 short 型。
而short s1 = 1; s1 += 1;可以正确编译,因为 s1+= 1;相当于 s1 = (short)(s1 + 1);其中有隐含的强制类型转换。
3.
浮点型
序号 | 数据类型 | 大小/位 | 封装类 | 默认值 | 可表示数据范围 | 存储需求 |
---|---|---|---|---|---|---|
5 | float(单精度) | 32 | Float | 0.0 | 1.4E-45~3.4028235E38 | 4 字节 |
6 | double(双精度) | 64 | Double | 0.0 | 4.9E-324~1.7976931348623157E308 | 8 字节 |
double 表示这种类型的数值精度是 float 类型的两倍
float 类型的数值有一个后缀(F 或者 f)没有后缀 F 的浮点数值(3.14)总是默认为 double 类型
浮点数值后面也可以加后缀(D 或者 d)
double 精度 大于 float 因此下用上必须强转(float f = (float) 1.23d)
字符型
序号 | 数据类型 | 大小/位 | 封装类 | 默认值 | 可表示数据范围 | 存储需求 |
---|---|---|---|---|---|---|
7 | char(字符) | 16 | Character | 空 | 0~65535 | 2 字节 |
boolean 型
序号 | 数据类型 | 大小/位 | 封装类 | 默认值 | 可表示数据范围 | 存储需求 |
---|---|---|---|---|---|---|
8 | boolean | 8 | Boolean | flase | true 或 false | 1 字节 |
数据类型转换
上图中,有 6 个实线箭头代表着无信息丢失的转换;另外有 3 个虚线箭头,代表着可能会有精度的丢失;
比如 123456789 是个大整数,它包含的位数比 float 类型所能够表达的位数要多;
那么我们看看 如果用 float 输出 这个整数会是什么情况:
我们可以看到,虽然大小差不多,但是还是损失了一点精度;
当用二元运算符连接 2 个值的时候,要把两个操作数转为一个类型,然后在进行运算,否则:
如果两个操作数中有一个是double类型,另一个操作数将被转换为double类型。
否则,如果其中一个操作数为float类型,另一个操作数将被转换为float类型。
否则,如果其中一个操作数为long类型,另一个操作数将被转换为long类型。
否则,两个操作数都将会被转换为int类型。
异常
- 检查性异常:用户错误或是问题引起的异常,是程序员无法预见的
- 运行时异常:是可能被程序员避免的异常,与检查性异常相反,运行时异常可以在编译时被忽略掉;
- 错误 ERROR:错误不是异常,而是脱离程序员控制的问题;比如栈溢出
异常体系结构
- Java 把异常当作对象来处理,并定义了一个基类 java.lang.Throwable 作为所有异常的超类
- Java API 中定义许多异常类,其中分为两大类:Error 和 Exception
Error
Error 类对象由 Java 虚拟机生成并抛出,大多数错误与代码编写者所执行的操作无关
Java 虚拟机运行错误(Virtual MachineError),当 JVM 不再由继续执行操作所需的内存资源的时候,将会出现OutOfMemoryError;这些异常发生的时候,JVM 一般会选择线程终止
还有的错误发生在虚拟机试图去执行应用的时候(比如类定义错误:NoClassDefFoundError),链接错误(LinkageError)
这些错误不可排查,因为他们在应用程序的控制和处理能力之外
Exception
Exception 分支有一个重要子类 RuntimeException(运行时异常)需要我们去掌握:
ArrayIndexOutOfBoundsException //数组下标越界 <!--code17-->
ArithmeticException //算术异常 比如0作为除数 <!--code18-->
ClassNotFoundException //找不到类 <!--code19-->
其实可以理解成:catch 就是平常公园里那种抓金鱼的网,try 中的部分就是我们的金鱼,只有你的网的大小合适,才能抓到金鱼;finally 就是我们玩抓金鱼要交的门票钱,无论抓不抓得到都得给,只不过是最后给 不是一开始给而已;
那我们难道只能 让程序跑的时候 抓异常,不能主动去抛出异常吗?
不是的,接下来还有介绍关键字throw throws
throw ≠ throws
- throw
- 作用在方法内,表示抛出具体异常,由方法体内的语句处理。
- 具体向外抛出的动作,所以它抛出的是一个异常实体类。若执行了 Throw 一定是抛出了某种异常。
- throws
- 作用在方法上,表示如果抛出异常,则由该方法的调用者来进行异常处理。
- 主要的声明这个方法会抛出会抛出某种类型的异常,让它的使用者知道捕获异常的类型。
- 出现异常是一种可能性,但不一定会发生异常。
例子:
throws E1,E2,E3 只是告诉程序这个方法可能会抛出这些异常,方法的调用者可能要处理这些异常,而这些异常 E1,E2,E3 可能是该函数体产生的。 throw 则是明确了这个地方要抛出这个异常。
1 | void doA(int a) throws Exception1,Exception3{ |
代码块中可能会产生 3 个异常,(Exception1,Exception2,Exception3)。
如果产生 Exception1 异常,则捕获之后再抛出,由该方法的调用者去处理。如果产生 Exception2 异常,则该方法自己处理了(即 System.out.println(“出错了!”);)。所以该方法就不会再向外抛出 Exception2 异常了,void doA() throws Exception1,Exception3 里面的 Exception2 也就不用写了
而 Exception3 异常是该方法的某段逻辑出错,程序员自己做了处理,在该段逻辑错误的情况下抛出异常 Exception3,则该方法的调用者也要处理此异常。
throw 语句用在方法体内,表示抛出异常,由方法体内的语句处理。throws 语句用在方法声明后面,表示再抛出异常,由该方法的调用者来处理。 throws 主要是声明这个方法会抛出这种类型的异常,使它的调用者知道要捕获这个异常。
throw 是具体向外抛异常的动作,所以它是抛出一个异常实例。 throws 说明你有那个可能,倾向。throw 的话,那就是你把那个倾向变成真实的了。
- throws 出现在方法函数头;而 throw 出现在函数体。
- throws 表示出现异常的一种可能性,并不一定会发生这些异常;throw 则是抛出了异常,执行 throw 则一定抛出了某种异常。
- 两者都是消极处理异常的方式,只是抛出/可能抛出异常,而不会去处理异常,真正的处理异常由函数的上层调用处理
自定义异常
在程序中使用自定义异常,大体可以分成下面几步:
- 创建自定义异常类
- 在方法中通过 throw 关键字抛出异常对象
- 如果在当前抛出异常的方法中处理异常,可以用try-catch 语句进行捕获处理;否则在方法的声明处通过throws关键字指明要抛出给方法调用者的异常,继续进行下一步操作
- 在出现一场方法的调用者中捕获并且处理异常
1 | //自定义异常类 |
1 | public class ExceptionTest { |
注解和反射
注解 Annotation
关于注解:https://www.cnblogs.com/fnlingnzb-learner/p/9723699.html
这篇文章起始也描述的也很好,把注解形容成了标签,我觉得非常生动形象
什么是注解
- Annotation 的作用
- 不是车舱内光绪本身,可以对程序做出解释
- 可以被其他程序(如编译器等)读取
- Annotation 的格式
- 注解是以==@注释名==在代码中存在的,还可以添加一些参数值,如 @SuppressWarnings(value=”unchecked”)
- Annotation 在哪里使用
- 可以附加在 package,class,method,field 等上面,相当于给他们添加了额外的辅助信息
- 我们可以通过反射机制编程实现对这些元数据的访问呢
内置注解(基本注解)
- @Override:定义在java.lang.Override中,此注解只适用于修饰方法,表示一个方法生命打算重写超类中的另一个方法声明
- @Deprecatd:定义在java.lang.Deprecatd中,此注释可以用于修饰方法,属性,类,表示不鼓励程序员使用这样的元素(废弃了)
- @SuppressWarning:定义在java.lang.SuppressWarnings中,用来抑制编译时的警告信息
- 这个注解需要添加一个参数才能使用:
- @SuppressWarning(“all”);
- @SuppressWarning(“unchecked”)
- @SuppressWarning(“value={“unchecked”, “deprecation”})
- 这个注解需要添加一个参数才能使用:
元注解
元注解的作用就是负责注解其他的注解,Java 定义了四个标准的meta-annotation 类型,他们被用来提供对其他 Annotation 类型的作说明
这些类型和他们所支持的类在java.lang.annotation包中
四个标准的 meta-annotation 类型分别是
@Target:用于描述注解的使用范围
![image-20201105150937346](
https://er11.oss-cn-shenzhen.aliyuncs.com/img/image-20201105150937346.png)
- @Target(ElementType.TYPE) //接口、类、枚举
@Target(ElementType.FIELD) //字段、枚举的常量
- @Target(ElementType.METHOD) //方法
@Target(ElementType.PARAMETER) //方法参数
- @Target(ElementType.CONSTRUCTOR) //构造函数
@Target(ElementType.LOCAL_VARIABLE)//局部变量
- @Target(ElementType.ANNOTATION_TYPE)//注解
@Target(ElementType.PACKAGE) ///包
@Retention:表示需要在什么级别(SOURCE < CLASS < RUNTIME)保存该注解信息,用于描述注解的生命周期
话句话说 就是我们的注解在什么地方还有效
运行级别 是 RUNTIME PentationPolicy 包含三个值:SOURCE CLASS RUNTIME- @Retention(RetentionPolicy.SOURCE) //注解仅存在于源码中,在class字节码文件中不包含 - @Retention(RetentionPolicy.CLASS) // 默认的保留策略,注解会在class字节码文件中存在,但运行时无法获得 - @Retention(RetentionPolicy.RUNTIME) // 注解会在class字节码文件中存在,在运行时可以通过反射获取到
- @Document:表示是否将我们的注解生成在 javadoc(java 文档)中
- @Inherited:说明子类可以继承父类中的该注解
https://er11.oss-cn-shenzhen.aliyuncs.com/img/image-20201105151542928.png)![image-20201105151542928](
- @Document:表示是否将我们的注解生成在 javadoc(java 文档)中
自定义注解
- 使用了@interface 自定义注解的时候,自动继承了java.lang.annotation.Annotation 接口
- 如何实现的呢?
- @interface 用来声明一个注解,格式:@interface 注解名{内容}
- 其中的每一个方法实际上是声明了一个配置参数
- 方法的名称其实就是参数的名称
- 返回值类型就是参数的类型(返回值只能是基本类型。或是类:Class,String,enum)
- 可以通过 default 来声明参数的默认值
- 如果只有一个参数成员,一般参数名为 value
- 注解元素必须要有值,我们定义注解元素的时候,经常使用空字符串,0 作为默认值
反射 Reflection
静态语言 动态语言
- 静态语言:运行时结构不可变的语言就是静态语言
- JAVA,C,C++
- 动态语言:是一类可以在运行的时候改变其结构的语言:例如新的函数、对象、甚至代码都可以被引进,已有的函数可以被删除或是其他结构上的变化。通俗点说就是在运行时,代码可以根据某些条件改变自身的结构
- Object - C,C#,JavaScript,PHP,Python
- Java 并不是动态语言,但 Java 可以被称为是准动态语言,即 Java 有一定的动态性,我们可以利用反射机制获取类似动态语言的特性;
java 反射机制
反射是 Java 被视为动态语言的关键,反射机制允许程序在执行期借助于==Reflection API==取得任何类的内部信息,并能直接操作任意对象的内部属性和方法
1
Class c = Class.forName("java.lang.String");
加载完类了之后,在堆内存的方法区中就产生了一个==Class==类型的对象(一个类只有一个 Class 对象)这个对象就包含了完整的类的结构信息;
我们可以通过这个对象看到类的结构;通过对象 → 看到类的结构,像在照镜子,因此叫==反射==
反射机制的功能
Java 反射优缺点
- 优点:可以实现动态创建对象和编译,体现出很大的灵活性
- 缺点:对性能有一定的影响;因为我们使用反射式一种解释操作,我们可以告诉 JVM,我们希望做什么,并且它满足我们的要求,这类操作总是慢于直接执行相同的操作 (一般来说用 new 创建对象 但是反射用 forName)
获取反射对象
1 | package com.hpg.Reflction; |
运行上述代码:
发现我们的 hash 值是一样的,代表着 一个类在内存中只会有一个 Class 对象;
此外,一个类被加载之后呢,类的整个的结构都会被封装在 Class 对象之中
理解 Class 类并获取 Class 实类
Class 类
在 Object 类中有以下这个方法:
1 | public final native Class<?> getClass(); |
此方法被所有的子类继承,这个方法的返回值类型为一个 Class 类,而这个 Class 类就是 Java 反射的源头了;
换句话说,我们通过对象反射出类的名称(比如张三是个人,张三是对象,人是个类,我们通过张三就能反射出 他是个人)
试想一下,假如张三(对象)早上起床,去照镜子,他能看见什么呢?
- 张三的样貌(属性)
- 张三在打哈欠(方法)
- ……
反射就是如此,对象通过反射能得 某个类的属性、方法、构造器、某个类实现了哪些接口;
而对于每个类而言,JRE 为其保留一个不变的 Class 类型的对象。就像是你的档案一样,包含各种信息;
形象点说就是,小孩子(对象)在外面走丢啦,想知道自己的父母是谁、家里在哪等等信息,可以去找公安局(Class 类),让他们帮你查询信息
Class 类的常用方法
常用方法列表
1 | 1 getName():返回String形式的该类的名称。 |
获取 Class 类实例
1 | package com.hpg.Reflction; |
获取类的方法:
- 已知具体的类,通过 class 属性获取
该方法最安全可靠,程序性能最高
已知某个类的实例(已经创建该类的对象了)
调用实例的 getClass()方法获取 Class 对象
已知一个类的类名了,且知道路径(包),可通过 Class 的静态方法 forName()获取
有可能找不到这个类,需要抛出ClassNotFoundException
内置的基本数据类型可以直接使用类名.Type
还可以使用 ClassLoader
验证 Class 对象:
目录结构:
1 | package com.hpg.Test; |
1 | //运行结果 |
带参构造方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50package com.hpg.Test;
public class Person {
private String name;
private int age;
private String address;
public Person() {
}
private Person(String name) {
this.name = name;
}
Person(String name, int age) {
this.name = name;
this.age = age;
}
public Person(String name, int age, String address) {
this.name = name;
this.age = age;
this.address = address;
}
public void show() {
System.out.println("show");
}
public void method(String s) {
System.out.println("method " + s);
}
public String getString(String s, int i) {
return s + "---" + i;
}
private void function() {
System.out.println("function");
}
public String toString() {
return "Person [name=" + name + ", age=" + age + ", address=" + address
+ "]";
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21package com.hpg.Test;
import java.lang.reflect.Constructor;
public class ReflectDemo2 {
public static void main(String[] args) throws Exception {
// 获取字节码文件对象
Class c = Class.forName("com.hpg.Test.Person");
// 获取带参构造方法对象
// public Constructor<T> getConstructor(Class<?>... parameterTypes)
Constructor con = c.getConstructor(String.class, int.class, String.class);
// 通过带参构造方法对象创建对象
// public T newInstance(Object... initargs)
Object obj = con.newInstance("hpg", 19, "深圳");
System.out.println(obj);
}
}
//Person [name=hpg, age=19, address=深圳]
getDeclared、getField 与 setAccessible(flag)
1 | package com.hpg.Test; |
1 | package com.hpg.Test; |
运行结果:
1 | public java.lang.String com.hpg.Test.Student.name |
通过以上代码我们可以发现:
假如属性是私有的,想要用 class 类去获取 该属性必须用:getDeclaredFields
这个方法可以获取类的全部字段
然而getFields
方法只能获取 public 字段
同时,想要通过反射去设置属性的话:如果是私有变量需要先xxx.setAccessible(true);
否则将没有权限
关于 setAccessible 原理:
应该是取消了 Java 的语言访问检查
1 | /** |
但我觉得这会不会在某种程度上破坏了对象的封装性呢?总感觉不太好
哪些类可以有 Class 对象
- class : 外部类,成员(成员内部类,静态内部类),局部内部类,匿名内部类
- interface 接口
- 数组
- enum 枚举
- annotation 注解
- primitive type 基本数据类型
- void
Java 内存分析
类的加载与 ClassLoader 的理解
- 加载:将 class 文件字节码内容加载到内存中,并且将这些静态数据转换成方法区的运行时数据结构,然后生成一个代表这个类的 java.lang.Class 对象
- 链接:将 Java 类的二进制代码合并到 JVM 的运行状态之中的过程,分为三步:验证,准备,解析
- 验证:确保加载的类信息符合 JVM 规范,没有安全方面的问题
- 准备:正式为类变量(static)分配内存并设置类默认初始值的阶段,这些内存都将在方法区中进行分配
- 解析:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址的过程)
- 初始化:
- 构造类构造器
()方法 (这是 jvm 去做的)的过程,类构造器()方法 是由编译器自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的- ps:类构造器是构造类信息的,不是构造该类对象的构造器
- 当初始化一个类的时候,如果该类的父类还没有进行初始化,先初始化其父类
- 虚拟机会保证一个类的
()方法在多线程环境中被正确加锁和同步
- 构造类构造器
1 | public class ClassTest01{ |
多线程
什么是线程?
线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。程序员可以通过它进行多处理器编程,你可以使用多线程对 运算密集型任务提速。比如,如果一个线程完成一个任务要 100 毫秒,那么用十个线程完成改任务只需 10 毫秒。
线程和进程有什么区别?
线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务。不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。别把它和栈内存搞混,每个线程都拥有单独的栈内存用来存储本地数据。
如何在 Java 中实现线程?
在语言层面有两种方式。java.lang.Thread 类的实例就是一个线程但是它需要调用 java.lang.Runnable 接口来执行,
由于线程类本身就是调用的 Runnable 接口所以你可以继承 java.lang.Thread 类或者直接调用 Runnable 接口来重写 run()方法实现线程。
用 Runnable 还是 Thread?
由于 Java 是不支持多继承的,因此我们选择继承哪个类就显得尤为重要了。同时,Java 支持调用多个接口,因此更多时候往往调用 Runnable 接口。
线程中 start()和 run()的区别
我们根据下面的代码 以及运行结果看一看:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51class DiffBewteenStartAndRun {
public static void main(String args[]) {
//Thread.currentThread().getName() 用于获取当前运行的线程的名字
System.out.println(Thread.currentThread().getName());
// creating two threads for start and run method call
Thread startThread = new Thread(new Task("start"));
Thread runThread = new Thread(new Task("run"));
startThread.start();
// new Thread
runThread.run();
//current Thread
startThread.run();
// current Thread;
runThread.start();
// new Thread
}
/*
* Simple Runnable implementation
*/
private static class Task implements Runnable {
private String caller;
public Task(String caller) {
this.caller = caller;
}
public void run() {
System.out.println("Caller: " + caller
+ " and code on this Thread is executed by : "
+ Thread.currentThread().getName());
}
}
}1
2
3
4
5
6//运行结果
main
Caller: run and code on this Thread is executed by : main
Caller: start and code on this Thread is executed by : Thread-0
Caller: start and code on this Thread is executed by : main
Caller: run and code on this Thread is executed by : Thread-1由打印结果我们发现:run 方法是在主线程中 main 中被调用的,run 方法运行在主线程 main 上;
start 方法会创建新的线程,而后再调用 run 方法,此时的 run 方法是运行创建的新线程上的;
线程的创建
三种创建 启动方式
继承 Thread 类
自定义线程类继承 Thread 类
重写 run 方法
创建线程对象,调用 start()启动线程(如果是 run 方法是当前线程中运行方法,不会创建新的线程)
启动线程:子类对象.start();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public class ThreadTest01{
public static void main(String[] args) {
//创建对象
MyThread1 mt1 = new MyThread1();
//调用start方法
mt1.start();
}
}
class MyThread1 extends Thread{
public void run() {
System.out.println("MyThread is running");
}
}
实现 Runnable 接口
自定义线程类实现 Runnable 接口
重写 run 方法
创建线程对象(这里一定要注意,如果只创建了实现接口的对象,是无法使用 start 方法的,只能用 run 方法),调用 start()启动线程(如果是 run 方法是当前线程中运行方法,不会创建新的线程)
启动线程:传入目标对象 → Thread 对象.start();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public class ThreadTest02 {
public static void main(String[] args) {
//创建Runnable接口实现类的实例
MyThread2 mt2 = new MyThread2();
//用该实例作为Thread的target来创建Thread对象
Thread mt = new Thread(mt2);
//调用start方法
mt.start();
}
}
class MyThread2 implements Runnable{
public void run() {
System.out.println("MyThread2 is running");
}
}
实现 Callable 接口
Thread(Runnable target)调用哪个 run 方法问题
问题引入:
以下代码中有两个 run 方法,一个是重写了 Thread 中的 run 方法(Run of Thread),一个是传入了 Runnable 对象,也就是现在的 Target 是 Runnable,由该对象去实现的 run 方法(Run of Runnable);(这里有涉及匿名内部类的知识,学习后再看这里会更清楚一点)
1
2
3
4
5
6
7
8
9
10new Thread(new Runnable() {
public void run() {
System.out.println("Run of Runnable");
}
}) {
public void run() {
System.out.println("Run of Thread");
}
}.start();运行结果:
问题扩展:
大体与上述代码并没有什么不同,但是在重写的代码中增加了
super.run();
1
2
3
4
5
6
7
8
9
10
11
12new Thread(new Runnable() {
public void run() {
System.out.println("Run of Runnable");
}
}) {
//这是重写的方法
public void run() {
System.out.println("Run of Thread");
super.run();
}
}.start();运行结果:
那么为什么是这样的结果呢?
查看 JDK 中 Thread 源码发现:
1 | private Runnable target; |
run 方法,首先检查 target 是否为空,如果不为空,执行 target 中的 run 方法
因此,我们就不难明白上述两个运行结果是怎么来的了:
对于第一个代码:
重写了 Thread 的 run()方法,同时传入了一个 Runnable 对象,该对象也实现了 run()方法。该 Thread 对象调用 start()方法后,会执行该对象重写的 run()方法,其输出结果也就是 Run of Thread,输出完后,run()方法返回,该线程对象的生命周期也就结束了。
对于第二个代码:
首先,该线程启动运行后,执行其重写的 run()方法,输出 Run of Thread。
接下来调用 super.run(),也就是调用超类的 run()方法,而该超类的 run()方法,也就是 JDK 定义的 Thread 类的 run(),其执行如上 JDK 源码 所示;显然 target 不为空,这时候会调用该对象的 run()方法,会输出 Run of Runnable.。
如果,上面的 Thread 并未重写 run()方法,那么,执行的结果还是一样。首先会执行该 Thread 的 run()方法,因为此时并未重写该方法,所以还是会调用 JDK 定以的 run()方法,也就是上面的代码段 ,在该代码段中会判断 target 是否为空,显然不是,所以会调用 Runnable 对象实现的 run()方法。
==总结==
对于形如:Thread(Runnable target ...)
,不管传入的 Target 是否为空,首先都会执行 Thread 自己的 run()方法。如果重写了该方法且该方法中没有 super.run(),那么是永远不会调用 Runnable 实现的 run()方法;
如果没有重写该方法,则会去判断 target 是否为空,以此来决定调用 target 实现的 run()方法;
如果重写了该方法,且该方法中有 super.run(),在执行完该语句之前的所有代码后,会判断 target 是否为空,以此来决定调用 target 实现的 run()方法,执行完后,接着执行该语句之后的代码。
普通方法调用和多线程区别
线程
线程状态
new(创建状态)
在程序中用构造方法创建了一个线程对象后,新的线程对象便处于新建状态,此时它已经有了相应的内存空间和其他资源,但还处于不可运行状态。新建一个线程对象可采用 Thread 类的构造方法来实现,例如 “Thread thread=new Thread()”。
就绪状态
新建线程对象后,调用该线程的 start() 方法就可以启动线程。当线程启动时,线程进入就绪状态。此时,线程将进入线程队列排队,等待 CPU 服务,这表明它已经具备了运行条件。
运行状态
当就绪状态被调用并获得处理器资源时,线程就进入了运行状态。此时,自动调用该线程对象的 run() 方法。run() 方法定义该线程的操作和功能。
阻塞状态/等待状态
一个正在执行的线程在某些特殊情况下,如被人为挂起或需要执行耗时的输入/输出操作,会让 CPU 暂时中止自己的执行,进入阻塞状态。在可执行状态下,如果调用 sleep(),suspend(),wait() 等方法,线程都将进入阻塞状态,发生阻塞时线程不能进入排队队列,只有当引起阻塞的原因被消除后,线程才可以转入就绪状态。
死亡状态
线程调用 stop() 方法时或 run() 方法执行结束后,即处于死亡状态。处于死亡状态的线程不具有继续运行的能力。
线程调度
调整线程的优先级:学过 OS 的同学们可能会对这个优先级比较敏感,在 Java 线程中优先级高(数字大)的线程拥有更多运行机会
Java 线程中的优先级以整数形式表达:1~10,1 最小,10 最大
Thread 类有以下三个静态常量:
static int MAX_PRIORITY
线程可以具有的最高优先级,取值为 10。
tatic int MIN_PRIORITY
线程可以具有的最低优先级,取值为 1。
static int NORM_PRIORITY
分配给线程的默认优先级,取值为 5。
其中,Thread 类设置优先级的方法为:
1
setPriority();
获得优先级的方法为:
1
getPriority();
需要注意的是:每个线程都有默认的优先级。主线程默认优先级是
static int NORM_PRIORITY
线程优先级存在继承关系,A 线程中创建 B 线程,B 与 A 享有相同优先级
线程睡眠:Thread.sleep(long millis)方法,使线程转到阻塞状态。millis 参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪(Runnable)状态。sleep()平台移植性好。
线程等待:Object 类中的 wait()方法,导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法。这个两个唤醒方法也是 Object 类中的方法,行为等价于调用 wait(0) 一样。
线程让步:Thread.yield() 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。
线程加入:join()方法,等待其他线程终止。在当前线程中调用另一个线程的 join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。
线程唤醒:Object 类中的 notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。 直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。类似的方法还有一个 notifyAll(),唤醒在此对象监视器上等待的所有线程。
线程休眠(sleep)
sleep(参数)参制定了线程阻塞/等待的毫秒数
这里当时有个疑问,欸?那我 sleep(0)有啥用啊?关于这个问题,其实阅读这篇文章就可以有解答:
https://blog.csdn.net/wl455624651/article/details/7388250
其实简单来说就是,触发操作系统立刻重新进行一次 CPU 竞争,不要小看这个功能,是很有必要的
sleep 存在异常
InterruptedException
sleep 时间结束后,线程从阻塞/等待状态 → 就绪状态
每个对象有一个锁,而sleep 不会释放锁!!
1 | import java.util.Date; |
1 | public class ThreadSleepDemo { |
输出结果:
通过运行结果,我们不难发现:每一组三个人名的打印顺序并不是一定的
这也印证了我们上面的说法:每当调用 sleep 函数后,线程回到了就绪队列,相当于回到了同一起跑线,需要进行新的一次 CPU 竞争
那么谁能抢到 CPU 呢?当然是说不定的啦,因此,打印顺序也是不一定的。
线程礼让(yield)
- 线程的礼让会使正在执行的线程暂停,但并不阻塞,也就是说该函数并不会让线程转移到等待/睡眠/阻塞状态
- 线程将会从运行态 → 就绪态
- 之后该让哪个线程运行,由 CPU 进行调度,因此有可能下个运行的还是那个礼让的线程
1 | public class ThreadYield extends Thread{ |
1 | public class ThreadYieldDemo { |
运行结果:
线程加入(join)
- Thread.join()很霸道,调用该方法的函数将会霸占处理机,等到该线程执行完成后,再执行别的线程,其他的线程在其此间为阻塞态
- 其实就是插队
- join 函数以及原理看起来简单,但是里面有很多细枝末节需要注意
https://blog.csdn.net/u013425438/article/details/80205693 这篇文章其实解释了得蛮清楚的
(ps:评论区也很值得一看)
1 | public class TestJoin { |
1 | B-1 |
当使用了 join 后:
1 | public class TestJoin { |
1 | A-1 |
然而,假如我们把t1.join()
放在t2.start()
之后,我们看看效果:
1 | public class TestJoin { |
1 | A-1 |
发现,又回到了交替执行的情况了,究竟是为什么呢?
查看 join()的源码:
1 | /** |
不难发现:join()方法的底层是利用 wait()方法实现的。
可以看出,join 方法是一个同步方法,当主线程调用 t1.join()方法时,主线程先获得了 t1 对象的锁,随后进入方法,调用了 t1 对象的 wait()方法,使主线程进入了 t1 对象的等待池,
此时,A 线程则还在执行,并且随后的 t2.start()还没被执行,因此,B 线程也还没开始。
等到 A 线程执行完毕之后,主线程继续执行,走到了 t2.start(),B 线程才会开始执行。
其实叽里呱啦说了一大堆,就说明了一个道理:在哪个线程调用了 xxx.join()方法,只会锁那个相应的线程
比如 a 线程调用了 t1.join(),那 a 就会被卡住,当然了,a 会不会对别的线程有影响就另当别论了
原文中用了一个例子来验证上述这个道理:
1 | public class TestJoin { |
发现是主线程跑完了后,ABC 线程交替执行
1 | main start |
当我们使用了 join 函数后:
1 | public class TestJoin { |
通过结果我们发现,主线程在调用 join 函数的地方停住了,后面是 AB 线程交替执行
这里有人会说啦:哎呀 不是只会停止主线程吗,为什么不是 ABC 线程交替执行啊?
那是因为我们的 C 线程此时还没有在主线程上启动,C 线程根本不存在,怎么执行呢
1 | main start |
如果你硬要改,让 C 也加入进来:
1 | public class TestJoin { |
1 | main start |
即证;
线程同步
并发:同一个对象被多个线程同时操作
线程同步基本思想:由于处理多线程问题的时候往往关乎到对同一个对象的访问,修改
因此我们需要线程同步:线程同步是一种等待机制,多个需要同时访问这个对象的线程的线程进入了这个对象的等待池
形成了队列,等待前面的线程使用完毕之后,下一个线程再使用
同步方法:队列 + 锁
锁机制
代理
代理模式分为两种:静态代理和动态代理
什么是代理呢?按照我自己的理解就是:“夹带私货”
什么意思呢?就是通过代理,我们可以实现我们一开始的目的,与此同时还会有一些额外的功能
来源:https://www.cnblogs.com/cC-Zhou/p/9525638.html
我们买东西虽然可以从厂家那直接拿,但一般来说都是从商店那买的;
他们也是从厂家那拿货,然后加一点小价钱卖给我们;
嗯?是不是有点熟悉,像不像我们 java 中的接口概念:我们和商店都实现了从 xxx 处买东西这一接口
在这个小例子中,商店就是那个代理,充当中间人角色,他加了一点小价钱 就是他夹带的私活 或者说是额外扩充的功能?
代理模式
代理模式是面向对象编程中比较常见的设计模式。
这是常见代理模式常见的 UML 示意图。
需要注意的有下面几点:
- 用户只关心接口功能,而不在乎谁提供了功能。上图中接口是 Subject。
- 接口真正实现者是上图的 RealSubject,但是它不与用户直接接触,而是通过代理。
- 代理就是上图中的 Proxy,由于它实现了 Subject 接口,所以它能够直接与用户接触。
- 用户调用 Proxy 的时候,Proxy 内部调用了 RealSubject。所以,Proxy 是中介者,它可以增强 RealSubject 操作。
静态代理
我们平常去电影院看电影的时候,在电影开始的阶段是不是经常会放广告呢?
电影是电影公司委托给影院进行播放的,但是影院可以在播放电影的时候,产生一些自己的经济收益,比如卖爆米花、可乐等,然后在影片开始结束时播放一些广告。
现在用代码来进行模拟。
首先得有一个接口,通用的接口是代理模式实现的基础。这个接口我们命名为 Movie,代表电影播放的能力。
1 | public interface Movie { |
然后,我们要有一个真正的实现这个 Movie 接口的类,和一个只是实现接口的代理类。
1 |
|
这个表示真正的影片。它实现了 Movie 接口,play() 方法调用时,影片就开始播放。那么 Proxy 代理呢?
1 | package com.frank.test; |
Cinema 就是 Proxy 代理对象,它有一个 play() 方法。不过调用 play() 方法时,它进行了一些相关利益的处理,那就是广告。现在,我们编写测试代码。
1 | package com.frank.test; |
结果:
1 | 电影马上开始了,爆米花、可乐、口香糖9.8折,快来买啊! |
动态代理
关键字
final
final 的简介
final 可以修饰变量,方法和类,用于表示所修饰的内容一旦赋值之后就不会再被改变,比如 String 类就是一个 final 类型的类。即使能够知道 final 具体的使用方法,我想对final 在多线程中存在的重排序问题也很容易忽略,希望能够一起做下探讨。
final 的具体使用场景
final 能够修饰变量,方法和类,也就是 final 使用范围基本涵盖了 java 每个地方,下面就分别以锁修饰的位置:变量,方法和类分别来说一说。
变量
在 java 中变量,可以分为成员变量以及方法局部变量。因此也是按照这种方式依次来说,以避免漏掉任何一个死角。
final 成员变量
通常每个类中的成员变量可以分为类变量(static 修饰的变量)以及实例变量。
针对这两种类型的变量赋初值的时机是不同的:
- 类变量可以在声明变量的时候直接赋初值或者在静态代码块中给类变量赋初值。
- 实例变量可以在声明变量的时候给实例变量赋初值,在非静态初始化块中以及构造器中赋初值。
即:类变量有两个时机赋初值,而实例变量则可以有三个时机赋初值。
当 final 变量未初始化时系统不会进行隐式初始化,会出现报错。这样说起来还是比较抽象,下面用具体的代码来演示。
看上面的图片已经将每种情况整理出来了,这里用截图的方式也是觉得在 IDE 出现红色出错的标记更能清晰的说明情况。现在我们来将这几种情况归纳整理一下:
- 类变量:必须要在静态初始化块中指定初始值或者声明该类变量时指定初始值,而且只能在这两个地方之一进行指定;
- 实例变量:必须要在非静态初始化块,声明该实例变量或者在构造器中指定初始值,而且只能在这三个地方进行指定。
final 局部变量
final 局部变量由程序员进行显式初始化,如果 final 局部变量已经进行了初始化则后面就不能再次进行更改,如果 final 变量未进行初始化,可以进行赋值,当且仅有一次赋值,一旦赋值之后再次赋值就会出错。下面用具体的代码演示 final 局部变量的情况:
现在我们来换一个角度进行考虑,final 修饰的是基本数据类型和引用类型有区别吗?
final 基本数据类型 VS final 引用数据类型
通过上面的例子我们已经看出来,如果 final 修饰的是一个基本数据类型的数据,一旦赋值后就不能再次更改,那么,如果 final 是引用数据类型了?这个引用的对象能够改变吗?我们同样来看一段代码。
1 | public class FinalExample { |
当我们对 final 修饰的引用数据类型变量 person 的属性改成 22,是可以成功操作的。
通过这个实验我们就可以看出来:
当 final 修饰基本数据类型变量时,不能对基本数据类型变量重新赋值,因此基本数据类型变量不能被改变。
而对于引用类型变量而言,它仅仅保存的是一个引用,final 只保证这个引用类型变量所引用的地址不会发生改变,即一直引用这个对象,但这个对象属性是可以改变的。
宏变量
利用 final 变量的不可更改性,在满足一下三个条件时,该变量就会成为一个“宏变量”,即是一个常量。
- 使用 final 修饰符修饰;
- 在定义该 final 变量时就指定了初始值;
- 该初始值在编译时就能够唯一指定。
注意:当程序中其他地方使用该宏变量的地方,编译器会直接替换成该变量的值
方法
重写?
当父类的方法被 final 修饰的时候,子类不能重写父类的该方法,比如在 Object 中,getClass()方法就是 final 的,我们就不能重写该方法,但是 hashCode()方法就不是被 final 所修饰的,我们就可以重写 hashCode()方法。我们还是来写一个例子来加深一下理解:
先定义一个父类,里面有 final 修饰的方法 test();
public class FinalExampleParent {
public final void test() {
}
}
然后 FinalExample 继承该父类,当重写 test()方法时出现报错,如下图:
通过这个现象我们就可以看出来被 final 修饰的方法不能够被子类所重写。
重载?
public class FinalExampleParent {
public final void test() {
}
public final void test(String str) {
}
}
可以看出被 final 修饰的方法是可以重载的。经过我们的分析可以得出如下结论:
1. 父类的 final 方法是不能够被子类重写的
2. final 方法是可以被重载的
类
当一个类被 final 修饰时,表名该类是不能被子类继承的。子类继承往往可以重写父类的方法和改变父类属性,会带来一定的安全隐患,因此,当一个类不希望被继承时就可以使用 final 修饰。还是来写一个小例子:
1 | public final class FinalExampleParent { |
父类会被 final 修饰,当子类继承该父类的时候,就会报错,如下图:
final 的例子
final 经常会被用作不变类上,利用 final 的不可更改性。我们先来看看什么是不变类。
不变类
不变类的意思是创建该类的实例后,该实例的实例变量是不可改变的。满足以下条件则可以成为不可变类:
- 使用 private 和 final 修饰符来修饰该类的成员变量
- 提供带参的构造器用于初始化类的成员变量;
- 仅为该类的成员变量提供 getter 方法,不提供 setter 方法,因为普通方法无法修改 fina 修饰的成员变量;
- 如果有必要就重写 Object 类 的 hashCode()和 equals()方法,应该保证用 equals()判断相同的两个对象其 Hashcode 值也是相等的。
JDK 中提供的八个包装类和 String 类都是不可变类,我们来看看 String 的实现。
1 | /** The value is used for character storage. */ |
可以看出 String 的 value 就是 final 修饰的,上述其他几条性质也是吻合的。