java对象初始化代码详解
作者:hapjin 发布时间:2023-09-19 13:59:01
本文主要记录JAVA中对象的初始化过程,包括实例变量的初始化和类变量的初始化以及final关键字对初始化的影响。另外,还讨论了由于继承原因,探讨了引用变量的编译时类型和运行时类型
一,实例变量的初始化
这里首先介绍下创建对象的过程:
类型为Dog的一个对象首次创建时,或者Dog类的static字段或static方法首次访问时,Java解释器必须找到Dog.class(在事先设定好的路径里面搜索);
找到Dog.class后(它会创建一个Class对象),它的所有static初始化模块都会运行。因此,static初始化仅发生一次——在Class对象首次载入的时候;
创建一个newDog()时,Dog对象的构建进程首先会在内存堆(Heap)里为一个Dog对象分配足够多的存储空间;
这种存储空间会清为零,将Dog中的所有基本类型(Primitive)设为它们的默认值(0用于数字,以及boolean和char的等价设定);
进行成员字段定义时发生的所有初始化都会执行;
执行构造函数。
然后,开始对实例变量进行初始化。一共有三种方式对实例变量进行初始化:
①定义实例变量时指定初始值
②非静态初始化块中对实例变量进行初始化
③构造器中对实例变量进行初始化
当new对象初始化时,①②要先于③执行。而①②的顺序则按照它们在源代码中定义的顺序来执行。
当实例变量使用了final关键字修饰时,如果是在定义该final实例变量时直接指定初始值进行的初始化(第①种方式),则:该变量的初始值在编译时就被确定下来,那么该final变量就类似于“宏变量”,相当于JAVA中的直接常量。
public class Test {
public static void main(String[] args) {
final String str1 = "HelloWorld";
final String str2 = "Hello" + "World";
System.out.println(str1 == str2);//true
final String str3 = "Hello" + String.valueOf("World");
System.out.println(str1 == str3);//false
}
}
第8行输出false,是因为:第7行中str3需要通过valueOf方法调用之后才能确定。而不是在编译时确定。
再来看一个示例:
public class Test {
final String str1 = "HelloWorld";
final String str2 = "Hello" + "World";
final String str3;
final String str4;
{
str3 = "HelloWorld";
}
{
System.out.println(str1 == str2);//true
System.out.println(str1 == str3);//true
// System.out.println(str1 == str4);//compile error
}
public Test() {
str4 = "HelloWorld";
System.out.println(str1 == str4);//true
}
public static void main(String[] args) {
new Test();
}
}
把第13行的注释去掉,会报编译错误“Theblankfinalfieldstr4maynothavebeeninitialized”
因为变量str4是在构造器中进行初始化的。而前面提到:①定义实例变量时直接指定初始值(str1和str2的初始化)、②非静态初始化块中对实例变量进行初始化(str3的初始化)要先于③构造器中对实例变量进行初始化。
另外,对于final修饰的实例变量必须显示地对它进行初始化,而不是通过构造器(<clinit>)对之进行默认初始化。
public class Test {
final String str1;//compile error---没有显示的使用①②③中的方式进行初始化
String str2;
}
str2可以通过构造器对之进行默认的初始化,初始化为null。而对于final修饰的变量 str1,必须显示地使用 上面提到的三种方式进行初始化。如下面的这个Test.java(一共有22行的这个Test类)
public class Test {
final String str1 = "Hello";//定义实例变量时指定初始值
final String str2;//非静态初始化块中对实例变量进行初始化
final String str3;//构造器中对实例变量进行初始化
{
str2 = "Hello";
}
public Test() {
str3 = "Hello";
}
public void show(){
System.out.println(str1 + str1 == "HelloHello");//true
System.out.println(str2 + str2 == "HelloHello");//false
System.out.println(str3 + str3 == "HelloHello");//false
}
public static void main(String[] args) {
new Test().show();
}
}
由于str1采用的是第①种方式进行的初始化,故在执行15行:str1+str1连接操作时,str1其实相当于“宏变量”
而str2和str3并不是“宏变量”,故16-17行输出false
在非静态初始化代码块中初始化变量和在构造器中初始化变量的一点小区别:因为构造器是可以重写的,比如你把某个实例变量放在无参的构造器中进行初始化,但是在new对象时却调用的是有参数的构造器,那就得注意该实例变量有没有正确得到初始化了。
而放在非静态初始化代码块中初始化变量时,不管是调用有参的构造器还是无参的构造器,非静态初始化代码块都会执行。
二,类变量的初始化
类变量一共有两个地方对之进行初始化:
❶定义类变量时指定初始值
❷静态初始化代码块中进行初始化
不管new多少个对象,类变量的初始化只执行一次。
三,继承对初始化的影响
主要是理解编译时类型和运行时类型的不同,从这个不同中可以看出this关键字和super关键字的一些本质区别。
class Fruit{
String color = "unknow";
public Fruit getThis(){
return this;
}
public void info(){
System.out.println("fruit's method");
}
}
public class Apple extends Fruit{
String color = "red";//与父类同名的实例变量
@Override
public void info() {
System.out.println("apple's method");
}
public void accessFruitInfo(){
super.info();
}
public Fruit getSuper(){
return super.getThis();
}
//for test purpose
public static void main(String[] args) {
Apple a = new Apple();
Fruit f = a.getSuper();
//Fruit f2 = a.getThis();
//System.out.println(f == f2);//true
System.out.println(a == f);//true
System.out.println(a.color);//red
System.out.println(f.color);//unknow
a.info();//"apple's method"
f.info();//"apple's method"
a.accessFruitInfo();//"fruit's method"
}
}
值得注意的地方有以下几个:
⒈第35行引用变量a和f都指向内存中的同一个对象,36-37行调用它们的属性时,a.color是red,而f.color是unknow
因为,f变量的声明类型(编译时类型)为Fruit,当访问属性时是由声明该变量的类型来决定的。
⒉第39-40行,a.info()和f.info()都输出“apple'smethod”
因为,f变量的运行时类型为Apple,info()是Apple重载的父类的一个方法。调用方法时由变量的运行时类型来决定。
⒊关于this关键字
当在29行new一个Apple对象,在30行调用getSuper()方法时,最终是执行到第4行的returnthis
this的解释是:返回调用本方法的对象。它返回的类型是Fruit类型(见getThis方法的返回值类型),但实际上是Apple对象导致的getThis方法的调用。故,这里的this的声明类型是Fruit,而运行时类型是Apple
⒋关于super关键字
super与this是有区别的。this可以用来代表“当前对象”,可用return返回。而对于super而言,没有returnsuper;这样的语句。
super主要是为了:在子类中访问父类中的属性或者在子类中调用父类中的方法而引入的一个关键字。比如第24行。
⒌在父类的构造器中不要去调用被子类覆盖的方法(Override),或者说在构造父类对象时,不要依赖于子类覆盖了父类的那些方法。这样很可能会导致初始化的失败(没有正确地初始化对象)
因为:前面第1点和第2点谈到了,对象(变量)有声明时类型(编译时类型)和运行时类型。而方法的调用取决于运行时类型。
当new子类对象时,会首先去初始化父类的属性,而此时对象的运行时类型是子类,因此父类的属性的赋值若依赖于子类中重载的方法,会导致父类属性得不到正确的初始化值。示例如下:
class Fruit{
String color;
public Fruit() {
color = this.getColor();//父类color属性初始化依赖于重载的方法getColor
// color = getColor();
}
public String getColor(){
return "unkonw";
}
@Override
public String toString() {
return color;
}
}
public class Apple extends Fruit{
@Override
public String getColor() {
return "color: " + color;
}
// public Apple() {
// color = "red";
// }
public static void main(String[] args) {
System.out.println(new Apple());//color: null
}
}
Fruit类的color属性 没有正确地被初始化为"unknow",而是为 null
主要是因为第5行 this.getColor()调用的是Apple类的getColor方法,而此时Apple类的color属性是直接从Fruit类继承的。
四,参考资料
疯狂Java 突破程序员基本功的16课 第二章
Effective Java中文版 第2版 中文 PDF版 第二版第17条
来源:https://www.cnblogs.com/hapjin/p/5931220.html
猜你喜欢
- Android自带的跑马灯效果不太好控制,还必须要满足条件才能有效果,而且速度不受控制。前面我的博客中有一篇就是用Android自带的跑马灯
- typora-copy-images-to: ./一键清除maven仓库中下载失败的jar包maven是一款非常优秀的项目管理工具,特别是其
- 我们都知道,当RecyclerView数据源更新后,还需要通过adapter调用对应的方法,从而让RecyclerView重新绘制页面本次也
- 你知道String、StringBuilder、Stringbuffer的区别吗?当你创建字符串的时候,有考虑过该使用哪个吗?别急,这篇文章
- 前言回想一下,在学Java时接触的正则表达式,其实Kotlin中也是类似。只不过使用Kotlin 的语法来表达,更为简洁。正则(Regex)
- android中提供了4中动画: AlphaAnimation 透明度动画效果 ScaleAnimation 缩放动画效果 Translat
- 一、介绍在实际的软件项目开发过程中,我可以很负责任的跟大家说,如果你真的实际写代码的时间超过5年,你对增删改查这类简单的功能需求开发,可以说
- 最新开发新项目的时候,要做分享项目,要求分享有微信,微信朋友圈,QQ,QQ空间,新浪微博这五个,所分享内容包括,分享纯图片,纯文字,图文类型
- 使用前准备Build.gradle文件配置dependencies配置compile 'com.squareup.retrofit2
- 本文实例为大家分享了android实现简易计算器展示的具体代码,供大家参考,具体内容如下效果图:一、如图,首先布局计算器主页显示activi
- ArrayList集合的创建非泛型创建ArrayList集合对象,可以添加任意Object子类元素至集合//非泛型创建的ArrayList集
- 引言在高并发的场景下,异步是一个极其重要的优化方向。前段时间,生产环境发生一次事故,笔者认为事故的场景非常具备典型性 。写这篇文章,笔者想和
- 简要:EigenFace是基于PCA降维的人脸识别算法,PCA是使整体数据降维后的方差最大,没有考虑降维后类间的变化。 它是将图像
- 多数据源配置首先是配置文件这里采用yml配置文件,其他类型配置文件同理我配置了两个数据源,一个名字叫ds1数据源,一个名字叫ds2数据源,如
- 合并有序数组的实现java版本:实例代码public class Merge {//合并有序数组 public static v
- cookie和session的区别和联系cookie是本地客户端用来存储少量数据信息的,保存在客户端,用户能够很容易的获取,安全性不高,存储
- 一、定义登录控制器目录结构代码:1、创建TUser类package com.demo.pojo;import lombok.AllArgsC
- 背景:在Android中按照数据保存的方式,可以分为如下几种Content Provider (用的SQLite实现),SQLite,Sha
- 前言Future的问题写多线程程序的时候,可以使用Future从一个异步线程中拿到结果,但是如果使用过程中会发现一些问题:如果想要对Futu
- 本文为大家分享一个非常简单但又很常用的控件,跑马灯状态的TextView。当要显示的文本长度太长,又不想换行时用它来显示文本,一来可以完全的