把描述对象属性的变量及实现对象功能的方法结合起来,定义一个程序单位,并保证外界不能任意更改内部属性,能任意调动内部的方法接口。
优点:
1.实现了低耦合高内聚;
2.类内部的结构可以自由修改;
3.隐藏实现细节,提出公共的访问方式。
大大增强了代码的可复用性,提高了软件的开发效率为程序的修改扩充了材料。继承通过extends关键字进行实现,并且java.long.Object类是所有类的父类。
局限性:
1.Java中继承是单继承,即一个子类只能继承一个父类;
2.支持多重继承,即一个父类可以有多个子类;
3.在继承中只能继承非私有化的属性和方法;
4.构造方法不能都被继承。
5.在继承时,子类的权限>=父类权限
(public>protected>默认>private)
多态是同一个行为具有多种不同的表现形式或形态的能力。允许不同类的对象对同一消息做出响应。即同一消息可以根据发送对象的不同而采用多种不同的行为方式。(发送消息就是函数调用)。
发生动态绑定(动多态)的三个条件:
1.继承
2.方法的重写
3.向上造型基类引引用引用派生类对象
优点:
1.可替换性。多态对已存在代码具有可替换性。例如,多态对Animal类设计,对其他任何动物,如狐狸,也同样具有吃饭的行为,以及工作的能力。
2.可扩充性。多态对代码具有可扩充性。增加新的子类不影响已存在类的多态性、继承性,以及其他特性的运行和操作。实际上新加子类更容易获得多态功能。例如:在实现了dog吃饭行为之后,还可以添加小狗特有的行为抓老鼠等。
3.接口性。多态是父类通过方法声明,向子类提供了一个共同接口,由子类来完善或者覆盖它而实现的。
首先介绍一下JVM的结构
当程序运行某个类时,这个类的class文件会通过类加载器进入到JVM中,并且会在方法区中建立该类的类型信息。
//父类
class Father{
public void A(){
System.out.println("Father");
}
public void A(int i){
System.out.println("Father"+i);
}
}
//子类
public class Child extends Father(){
public void A(){
System.out.println("Child");
}
public void A(char c){
System.out.println("Father"+c);
}
public static void main(String[]args){
Father father=new Child():
father.A()://Child
}
}
以上代码的结果输出是:Child
JVM在调用father.A()方法时怎么知道指向的是Father的方法还是Child的方法?
因为在JVM加载类的同时,会在方法区中为这个类存放很多信息。其中就有一个数据结构叫方法表。在方法表中,子类继承了父类的方法,故它们的方法都存放在各自表的相同位置。
根据father的声明类型(Father)其实不能够确定调用方法A,必须根据father在堆中实际创建的对象类型Child来确定A方法所在的位置。这种在程序运行过程中,通过动态创建的对象的方法表来定位方法的方式,我们叫做动态绑定机制 。
public static void main(String[]args){
Father father=new Child():
char c='a';
father.A()://Father96
在代码中可以清楚看到Father类中并没有A(char c)方法,但结果显示调用了Child中的A(char c)方法,JVM首先是根据对象father声明的类型Father来解析常量池的(也就是用Father方法表中的索引项来代替常量池中的符号引用)。如果Father中没有匹配到"合适" 的方法,就无法进行常量池解析,这在编译阶段就通过不了。
如果方法中的参数类型在声明的类型中并不能找到呢?比如上面的代码中调用father.A(char),Father类型并没有A(char)的方法签名。实际上,JVM会找到一种“凑合”的办法,就是通过 参数的自动转型 来找 到“合适”的 方法。比如char可以通过自动转型成int,那么Father类中就可以匹配到这个方法了。
class Father{
public void A(Object o){
System.out.println("Object");
}
public void A(double[] d){
System.out.println("double[]");
}
}
public class Demo{
public static void main(String[] args) {
new Father().f1(null); //打印结果: double[]
}
}
null可以引用于任何的引用类型,这时就有一个标准:如果一个方法可以接受传递给另一个方法的任何参数,那么第一个方法就相对不合适。比如上面的代码: 任何传递给A(double[])方法的参数都可以传递给A(Object)方法,而反之却不行,那么A(double[])方法就更合适。因此JVM就会调用这个更合适的方法。
在三大特征的基础上添了一个抽象性。此处只介绍一下抽象性。
final关键字从三个方面来说:变量、方法、类
1.变量:final int INITSIZE=10;常量不能被修改
2.方法:final void fun(){} 方法不能被重写
3.类: final class String{} 方法不能被继承
static关键字也从三个方面来说明:变量、方法、类
变量
静态变量 | 实例变量 | |
---|---|---|
存储位置 | 静态变量在方法去存储 | 实例变量在堆中存储 |
使用 | 静态变量鱼类有关,一个类只能有一个静态变量 | 实例变量与对象有关,几个对象几个实例 |
方法
静态方法 | 实例方法 | |
---|---|---|
调用方式 | 依赖于类,通过类的静态方法调用 | 依赖于类的对象,需要创建对象后通过实例方法使用 |
使用 | 静态方法内部可以定义和使用实例变量,但无法直接调用实例方法,静态方法不能被重写 | 实例方法内部不能定义静态变量,但可以直接调用是静态方法 |
类:类只能用来修饰内部类,静态内部类和实例内部类的区别
静态内部类 | 非静态内部类 | |
---|---|---|
声明 | 不依赖于外部类对象 | 依赖于外部类对象 |
定义 | 类变量、类方法、常量、成员变量和方法 | 常量、成员变量和方法 |
访问 | 能访问外部类的静态成员变量和静态方法,不能引用外部类的对象(this) | 访问外部类的所有成员和方法 |
类的初始化顺序
无继承:
静态变量/静态代码块 -> main方法 -> 非静态变量/代码块 -> 构造方法
继承:
1.子类的静态变量和静态初始化块的初始化是在父类的变量、初始化块和构造器初始化之前就完成了;
2.静态变量、静态初始化块顺序取决于它们在类中出现的先后顺序
3.变量、初始化块初始化顺序取决于它们在类中出现的先后顺序
单例模式的条件:
1.构造函数私有
2.公有静态函数返回实例对象
饿汉单例的对象是被修饰成final类型。
饿汉单例首先就会创建一个对象。
线程是否安全:安全(由类加载机制保证)
懒汉单例创建了延迟对象,尽可能的节省内存空间。
线程是否安全:不安全
在多线程编程中,使用懒汉式可能造成类的对象在内存中不唯一,虽然用过修改代码可以改正这些问题,但是降低了效率线程不安全。
线程是否安全:安全
线程同步时的效率不高
第一种:同步代码块
灵活
synchronized(线程共享对象){
同步代码块;
}
第二种:在实例方法上使用synchronized
表示共享对象一定是this
并且同步代码块是整个方法体。
第三种:在静态方法上使用synchronized
表示找类锁。
类锁永远只有1把。
就算创建了100个对象,那类锁也只有一把锁
线程是否安全:安全
类主动加载时才初始化实例,实现了懒加载策略
线程是否安全:安全
其实现了懒加载的策略,同时也保证了线程同步时的效率。使用volatile关键字时,会强制当前每次读操作进行时,保证其他所有线程写操作已完成,使得了JVM内部的编译器舍弃了编译时优化,对于性能有一定的影响。
public class Single5{
private static volatile Single5 singleton5;
private Single5(){}
public static Single5 getInstance(){
if(singleton5==null){
synchronized(Single5.class){
if(singleton5==null){
//内层if判断使用的时间(即起作用的时间)
//第一次两线程同时调用getInstance,都会进入外层if判断
//内层if判断是针对第二个synchronized代码块线程,此时第一个线程已经创建出对象,第二个线程无需创建
singleton5=new Single5();
}
}
return singleton5;
线程是否安全:安全
不存在线程同步问题,且单例对象在程序第一次getInstance()时主动加载SingletonHolder 和 静态成员INSTANCE
线程是否安全:安全
不存在线程同步问题,且单例对象在枚举类型INSTANCE,第一次引用时通过枚举的 构造函数 初始化。
这种方式是Effective Java 书籍提倡的方式。它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。
类加载过程主要是加载–连接–初始化,三个方面,其中连接又可以分为验证–准备–解析。
invokesstatic
专门调用静态方法invokespecial
用来调用构造invokevirtual
用来调用实例父类静态变量 父类静态块
子类静态变量 子类静态块
父类的实例变量、实例块、构造
子类的实例变量、实例块、构造
加载
1.通过全类名获取定义此类的二进制字节流,class文件
2.将字节流所代表的静态存储结构转换为方法区运行时的数据结构,即存储到方法区
3.在内存中生成一个代表该类的java.lang.Class 对象,作为方法区这些数据的访问入口
为什么采用双亲委派模型,优点是什么?
双亲委派模型优点:
(1)安全性,避免用户自己编写的类动态替换Java的一些核心类。如果不采用双亲委派模型的加载方式进行类的加载工作,那我们就可以随时使用自定义的类来动态替代Java核心API中定义的类。例如:如果黑客将“病毒代码”植入到自定义的String类当中,随后类加载器将自定义的String类加载到JVM上,那么此时就会对JVM产生意想不到“病毒攻击”。而双亲委派的这种加载方式就可以避免这种情况,因为String类已经在启动时就被引导类加载器进行了加载。
(2)避免类的重复加载,因为JVM判定两个类是否是同一个类,不仅仅根据类名是否相同进行判定,还需要判断加载该类的类加载器是否是同一个类加载器,相同的class文件被不同的类加载器加载得到的结果就是两个不同的类。
验证
验证的阶段是十分耗时的,它是重要但非必须。验证是为了使得防止字节流中有安全问题产生。Java的安全性是由编译器所保证的,而类加载是在虚拟机中进行的,当无法确保来源的时候则需要验证。
文件格式验证----->元数据验证----->字节码验证---->符号引用验证
文件格式验证
这个阶段主要验证输入的二进制字节流是否符合class文件结构的规范。二进制字节流只有通过了本阶段的验证,才会被允许存入到方法区中。
其是基于二进制字节流的
加载和验证是交叉进行的,在加载开始后,立即启动了文件格式验证,本阶段验证通过后,二进制字节流被转换成特定数据结构存储至方法区中,继而开始下阶段的验证和创建Class对象等操作。
元数据验证
本阶段对方法区中的字节码描述信息进行语义分析,确保其符合Java语法规范。基于类特定的数据结构的。
字节码验证
本阶段是验证过程的最复杂的一个阶段。通过数据流和控制流分析,确定程序语义是合法的,符合逻辑的。基于类特定的数据结构的。
符号引用验证
本阶段验证发生在解析阶段,确保解析能正常执行。基于类特定的数据结构的。
准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配,类的静态成员变量也存储在方法区中。
为静态成员变量设置初始值初始值为0、false、null等。
解析
解析阶段是虚拟机将常量池中的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符7类符号引用进行。
初始化
初始化阶段就是执行类构造器()的过程,是类加载的最后一步。
对于方法的调用,虚拟机会自己确保其在多线程环境中的安全性。因为 () 方法是带锁线程安全,所以在多线程环境下进行类初始化的话可能会引起死锁,并且这种死锁很难被发现。
必须对类进行初始化的情况
1.当遇到 new 、 getstatic、putstatic或invokestatic 这4条直接码指令时,比如 new 一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。
2.使用 java.lang.reflect 包的方法对类进行反射调用时 。
3.初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。
4.当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类),虚拟机会先初始化这个类。