Java类与对象查缺补漏(一)
本文不会大篇幅的讲类与对象的基础,主要针对面试 以及我在学习类与对象过程中的疑难和困惑
- 方法的重载和重写
- static关键字
- final关键字
- 访问修饰符之protected
- Java对象的创建过程
1、方法的重载和重写
1.1方法的重载(overload)
方法的重载的条件:
- 必须在同一个类中
- 方法名一样
- 参数列表的参数的个数、参数的类型,或者不同类型参数位置不一样
- 与返回值和访问修饰符无关
- 下列代码中add方法就是方法重载的例子
public class Calculator {
public int add(int a , int b){
return a+b;
}
public double add1(double a , double b){
return a+b;
}
public double add(double a , int b){
return a+b;
}
public double add( int a ,double b){
return a+b;
}
private void add(){
System.out.println(1+2);
}
}
1.2、方法的重写(override)
- 如果子类对父类某个方法的实现不满意,子类可以去重写父类的方法(方法体)
方法重写的条件:
- 子类重写父辈类的方法
- 方法的方法名、参数列表与父类被重写的方法一样
- 子类方法的访问修饰符 不能小于,比父类被重写的方法的访问修饰符
- 子类重写方法的返回值 等于 父类被重写方法的返回值类型
- 重写方法不能抛出新的异常或者比被重写方法声明的检查异常更广的检查异常。但是可以抛出更少,更有限或者不抛出异常。
2、static关键字
- static用来声明全局变量或是被其他对象引用的变量。
static String str=new String("Beau"); //报错
- static作用就是将 实例成员变为类成员。 static只能修饰在类里定义的成员部分,包括成员变量、方法、内部类(枚举、接口)、初始化块。
- 没有使用static修饰,这些成员属于该类的实例
- 使用了static修饰,这些成员就属于类本身
- static属性的赋默认值和初始化时机
public class Father{
public static int a=1;
}
我们都知道类的生命周期分为5个阶段:加载 链接 初始化 使用 卸载
类的加载包含:加载 链接 和 初始化 三个阶段
加载:指类加载的加载阶段
链接包含:验证->准备->解析
在准备阶段,会为静态变量赋默认值
- 即准备阶段结束后 a=0
接下来初始化阶段,这里是指类加载的初始化阶段: 为类的静态变量和 静态非字面值常量 (执行clinit)
- 何为字面常量: 整型 浮点型 字符型 布尔型 字符串字面常量( 双引号括起来的0个或多个字符构成的 , 字符串字面常量的类型总是
String
)- 如何区别静态非字面常量 和 静态字面常量
staic final String=get(); 非字面 ( get为静态方法返回 “Beau” )
static final String =“Beau” 字面
- 为什么静态字面常量的赋值 不在 Clinit()时
涉及到JVM的优化,详情请看下面关于final的讲解
初始化阶段后 a=1;
-
这里我们不得不涉及一个知识点关于静态初始化块,同样也在Clinit()时调用
public class Father{ public static int a=1; //静态初始化块 static { a=2; } }
3、final关键字
- 被final修饰的类,不能被继承 像包装类型,String
public final class String implements *** {
/** The value is used for character storage. */
private final char value[];
}
//final对String的好处
//1.提高效率:将String作为唯一值,可以直接对其hash进行缓存,减少重复运算
//2.提高资源的利用率:final修饰的char数组,这样做的好处在于,Java中的String对象是被放到常量池的,可以被多个String对象共同持有,字符串常量池避免了重复String对象的创建,节省了内存资源,同时由于减少了对象创建的次数,也提高了程序的执行效率
//3.保证线程安全:String是不可变的,因此不必担心String对象会被其他线程改变,天生线程安全
- final修饰的方法不可被重写
- final修饰的变量
- 修饰成员变量: final修饰的成员变量必须在声明的时候初始化或者在构造器中初始化,否则就会报编译错误;
- 局部变量:final修饰的局部变量必须声明时赋值,如果不赋值,虽然声明是不会出错,但调用时会编译出错;
final关键字的好处
- 提高性能,JVM和Java应用都会缓存final变量
- 线程安全
- final static作为常量时(静态字面常量)进行优化 (36条消息) 深入java final关键字 用法注意点和JVM对其进行的优化(例子)_yabay2208的博客-CSDN博客
public class Father{
static {
System.out.println("Fater被初始化");
}
stat
final static String str= "Beau"; //所谓的静态字面常量
}
public class Test1 {
public static void main(String[] args) {
System.out.println(Father.str);
}
}
Test1执行结果: 静态字面常量被JVM优化了,不会加载Father类
public class Father{
static {
System.out.println("Fater被初始化");
}
static String getStr(){
return "Beau";
}
final static String str= getStr(); //静态非字面常量
}
public class Test2 {
public static void main(String[] args) {
System.out.println(Father.str);
}
}
Test2执行结果,可见静态非字面常量 初始化走了Clinit()
3.1关于字面量的考究,又不得不联系到常量池 (涉及Class文件系统)
转载于 (36条消息) 详解JVM常量池、Class常量池、运行时常量池、字符串常量池(心血总结)_祈祷ovo的博客-CSDN博客_jvm运行时常量池
3.1.1Class常量池
常量池,即Class常量池.Java文件被编译为Class文件,**Class文件中除了包含类的版本,字段,方法 、接口等描述信息外,还有一项就是常量池 **, 常量池是当Class文件被Java虚拟机加载进来后存放在方法区 各种字面量 (Literal)和 符号引用 。
Class文件结构中,前4个字节用于存储魔数,用于确定一个文件是否能被JVM接受,再接着4个字节用于存储版本号, 前2个字节存储次版本号,后2个存储主版本号, 再接着是用于存放常量的常量池主要用于存放两大类常量:字面量和符号引用量,字面量相当于Java语言层面常量的概念(被final static修饰),如文本字符串,声明为final的常量值,符号引用则属于编译原理的概念
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-knSb2Qr7-1658559525304)(C:\Users\Administrator\Desktop\JavaSe\assets\3_5.png)]
3.1.2运行时常量池
运行时常量池是方法区的一部分.运行时常量池是当Class被加载到内存后,JVM会将Class文件常量池的内容转移到运行常量池里(运行时常量池每个类都有一个).运行时常量池相对于Class文件常量池的另外一个特征是具备动态性,Java语言并不要求常量一定只有编译器才能产生, 也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中
3.1.3字符串常量池
String类是我们使用频率非常高的一种对象类型。JVM为了提升性能和减少内存开销,避免字符串的重复创建,其维护了一块特殊的内存空间 . 即字符串常量池。字符串常量池由String类私有的维护。
关于字符串常量池详情: (36条消息) 详解JVM常量池、Class常量池、运行时常量池、字符串常量池(心血总结)_祈祷ovo的博客-CSDN博客_jvm运行时常量池
4、访问权限问题之protected
问题来自Object.clone()使用时碰到的protected权限问题
protected修饰的属性和方法,在不同包的时候,子类可以访问该属性和方法,但不能在包外创建新的对象调用
protected权限成员可以被非同包的子类访问,即子类内部可以直接使用父类的protected成员
但是如果子类在非父类所在包 外部访问父类的protected成员
package packge1;
public class Father {
protected void say(){
System.out.println("调用成功");
}
}
package packge2;
import packge1.Father;
public class Son extends Father {
public void use(){
this.say(); //子类内部调用,可以在不同包
}
}
package packge2;
public class Test {
public static void main(String[] args) {
Son son=new Son();
son.use(); //可以使用
// son.say(); //编译异常
}
}
关于解决这个问题,可以通过子类重写父类的这个方法,修改访问修饰符
对于clone方法,也可以通过实现Cloneable接口 实现clone()方法解决
5、Java对象创建过程
5.1Java对象的创建方式
- new关键字
最常见最简单的创建对象的方式,通过这种方式我们可以调用类的构造器去创建对象
- 通过反射创建
无参构造通过 Class.newInstance 有参通过Constructor.newInstance,
关于反射的内容可以参考我的文章 (36条消息) 反射与注解_Beau想躺平的博客-CSDN博客
- Object.clone方法
Object中的clone方法是浅拷贝
- 反序列化方式
当我们反序列化一个对象时,JVM会给我们创建一个单独的对象,在此过程中,JVM并不会调用任何构造函数。为了反序列化一个对象,我们需要让我们的类实现Serializable接口。
5.2Java对象内存布局
对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据和 对其填充(Padding)
- 对象头
对象头部分由:MarkWord、指向类的指针、数组长度(只有数组对象才有)
MarkWord主要存储对象在运行时的数据,如哈希码、GC分代年龄、锁状态标识、线程持有的锁、偏向锁ID等,
指向类的指针:Java对象的类数据保存在方法区
数组长度:只有数组对象保存这部分数据 – 涉及到两种java对象访问方式:直接指针访问和句柄访问
- 实例数据
对象的实例数据就是在Java代码中能看到的属性
和他们的属性值
- 对其填充
因为JVM要求Java的对象占的内存大小应该是8bit的倍数,所以后面有几个字节用于把对象的大小补齐至8bit的倍数,没有特别的功能。
5.3Java对象创建过程
1.检查类是否已被加载
new关键字创建对象时,首先回去运行时常量池查找该引用所指向的类有没有被虚拟机加载,如果没有被加载,那么会进行类的加载过程.类的加载过程:加载 、 链接 、 初始化三个阶段
关于类加载或JVM相关知识可以查看该博主 (36条消息) Java类的加载机制_骑个小蜗牛的博客-CSDN博客_java 类的加载机制
2.为对象分配内存空间
对象所属类已被加载,现在需要向堆区为该对象分配一定的空间, 该空间的大小在类加载完成时就已经确定下来了。
3.为对象的字段赋默认值
分配完内存后,需要对对象的字段进行初始化(赋默认值),除对象头
4.设置对象头
对这个将要创建出来的对象,进行信息标记,包括是否为新生代/老年代,对象的哈希码,元数据信息,这些标记存放在对象头信息中。
5.执行实例的初始化方法linit() 区别于类加载的 初始化阶段的Clinit()
Clinit方法包含静态成员变量 静态代码块的初始化,同样按照声明的顺序执行。
linit方法包含成员变量、构造代码块的初始化,同样按照声明的顺序执行。
6.执行构造方法。
执行对象的构造方法。至此,对象创建成功。
上述为无父类的对象创建过程.对于有父类的对象创建,还需
1.先加载父类,再加载本类 (先执行父类Clinit 后执行 子类的Clinit)
2.先执行父类的实例的初始化方法linit(包括成员变量和构造代码块),父类的构造方法;在执行本类的实例的初始化方法linit(成员变量、构造代码块),本类的构造方法。