六、初始化和清理(3)

news2024/11/26 22:22:59

本章概要

  • 成员初始化
  • 构造器初始化
    • 初始化的顺序
    • 静态数据的初始化
    • 显式的静态初始化
    • 非静态实例初始化

成员初始化

Java 尽量保证所有变量在使用前都能得到恰当的初始化。对于方法的局部变量,这种保证会以编译时错误的方式呈现,所以如果写成:

void f() {
    int i;
    i++;
}

你会得到一条错误信息,告诉你 i 可能尚未初始化。编译器可以为 i 赋一个默认值,但是未初始化的局部变量更有可能是程序员的疏忽,所以采用默认值反而会掩盖这种失误。强制程序员提供一个初始值,往往能帮助找出程序里的 bug。

要是类的成员变量是基本类型,情况就会变得有些不同。正如在"万物皆对象"一章中所看到的,类的每个基本类型数据成员保证都会有一个初始值。下面的程序可以验证这类情况,并显示它们的值:

// housekeeping/InitialValues.java
// Shows default initial values

public class InitialValues {
    boolean t;
    char c;
    byte b;
    short s;
    int i;
    long l;
    float f;
    double d;
    InitialValues reference;

    void printInitialValues() {
        System.out.println("Data type Initial value");
        System.out.println("boolean " + t);
        System.out.println("char[" + c + "]");
        System.out.println("byte " + b);
        System.out.println("short " + s);
        System.out.println("int " + i);
        System.out.println("long " + l);
        System.out.println("float " + f);
        System.out.println("double " + d);
        System.out.println("reference " + reference);
    }

    public static void main(String[] args) {
        new InitialValues().printInitialValues();
    }
}

输出:

在这里插入图片描述

可见尽管数据成员的初值没有给出,但它们确实有初值(char 值为 0,所以显示为空白)。所以这样至少不会出现"未初始化变量"的风险了。

在类里定义一个对象引用时,如果不将其初始化,那么引用就会被赋值为 null

指定初始化

怎么给一个变量赋初值呢?一种很直接的方法是在定义类成员变量的地方为其赋值。以下代码修改了 InitialValues 类成员变量的定义,直接提供了初值:

// housekeeping/InitialValues2.java
// Providing explicit initial values

public class InitialValues2 {
    boolean bool = true;
    char ch = 'x';
    byte b = 47;
    short s = 0xff;
    int i = 999;
    long lng = 1;
    float f = 3.14f;
    double d = 3.14159;
}

你也可以用同样的方式初始化非基本类型的对象。如果 Depth 是一个类,那么可以像下面这样创建一个对象并初始化它:

// housekeeping/Measurement.java

class Depth {}

public class Measurement {
    Depth d = new Depth();
    // ...
}

如果没有为 d 赋予初值就尝试使用它,就会出现运行时错误,告诉你产生了一个异常(详细见"异常"章节)。

你也可以通过调用某个方法来提供初值:

// housekeeping/MethodInit.java

public class MethodInit {
    int i = f();
    
    int f() {
        return 11;
    }
    
}

这个方法可以带有参数,但这些参数不能是未初始化的类成员变量。因此,可以这么写:

// housekeeping/MethodInit2.java

public class MethodInit2 {
    int i = f();
    int j = g(i);
    
    int f() {
        return 11;
    }
    
    int g(int n) {
        return n * 10;
    }
}

但是你不能这么写:

// housekeeping/MethodInit3.java

public class MethodInit3 {
    //- int j = g(i); // Illegal forward reference
    int i = f();

    int f() {
        return 11;
    }

    int g(int n) {
        return n * 10;
    }
}

显然,上述程序的正确性取决于初始化的顺序,而与其编译方式无关。所以,编译器恰当地对"向前引用"发出了警告。

这种初始化方式简单直观,但有个限制:类 InitialValues 的每个对象都有相同的初值,有时这的确是我们需要的,但有时却需要更大的灵活性。

构造器初始化

可以用构造器进行初始化,这种方式给了你更大的灵活性,因为你可以在运行时调用方法进行初始化。但是,这无法阻止自动初始化的进行,他会在构造器被调用之前发生。因此,如果使用如下代码:

// housekeeping/Counter.java

public class Counter {
    int i;
    
    Counter() {
        i = 7;
    }
    // ...
}

i 首先会被初始化为 0,然后变为 7。对于所有的基本类型和引用,包括在定义时已明确指定初值的变量,这种情况都是成立的。因此,编译器不会强制你一定要在构造器的某个地方或在使用它们之前初始化元素——初始化早已得到了保证。,

初始化的顺序

在类中变量定义的顺序决定了它们初始化的顺序。即使变量定义散布在方法定义之间,它们仍会在任何方法(包括构造器)被调用之前得到初始化。例如:

// housekeeping/OrderOfInitialization.java
// Demonstrates initialization order
// When the constructor is called to create a
// Window object, you'll see a message:

class Window {
    Window(int marker) {
        System.out.println("Window(" + marker + ")");
    }
}

class House {
    Window w1 = new Window(1); // Before constructor

    House() {
        // Show that we're in the constructor:
        System.out.println("House()");
        w3 = new Window(33); // Reinitialize w3
    }

    Window w2 = new Window(2); // After constructor

    void f() {
        System.out.println("f()");
    }

    Window w3 = new Window(3); // At end
}

public class OrderOfInitialization {
    public static void main(String[] args) {
        House h = new House();
        h.f(); // Shows that construction is done
    }
}

输出:

Window(1)
Window(2)
Window(3)
House()
Window(33)
f()

House 类中,故意把几个 Window 对象的定义散布在各处,以证明它们全都会在调用构造器或其他方法之前得到初始化。此外,w3 在构造器中被再次赋值。

由输出可见,引用 w3 被初始化了两次:一次在调用构造器前,一次在构造器调用期间(第一次引用的对象将被丢弃,并作为垃圾回收)。这乍一看可能觉得效率不高,但保证了正确的初始化。试想,如果定义了一个重载构造器,在其中没有初始化 w3,同时在定义 w3 时没有赋予初值,那会产生怎样的后果呢?

静态数据的初始化

无论创建多少个对象,静态数据都只占用一份存储区域。static 关键字不能应用于局部变量,所以只能作用于属性(字段、域)。如果一个字段是静态的基本类型,你没有初始化它,那么它就会获得基本类型的标准初值。如果它是对象引用,那么它的默认初值就是 null

如果在定义时进行初始化,那么静态变量看起来就跟非静态变量一样。

下面例子显示了静态存储区是何时初始化的:

// housekeeping/StaticInitialization.java
// Specifying initial values in a class definition

class Bowl {
    Bowl(int marker) {
        System.out.println("Bowl(" + marker + ")");
    }
    
    void f1(int marker) {
        System.out.println("f1(" + marker + ")");
    }
}

class Table {
    static Bowl bowl1 = new Bowl(1);
    
    Table() {
        System.out.println("Table()");
        bowl2.f1(1);
    }
    
    void f2(int marker) {
        System.out.println("f2(" + marker + ")");
    }
    
    static Bowl bowl2 = new Bowl(2);
}

class Cupboard {
    Bowl bowl3 = new Bowl(3);
    static Bowl bowl4 = new Bowl(4);
    
    Cupboard() {
        System.out.println("Cupboard()");
        bowl4.f1(2);
    }
    
    void f3(int marker) {
        System.out.println("f3(" + marker + ")");
    }
    
    static Bowl bowl5 = new Bowl(5);
}

public class StaticInitialization {
    public static void main(String[] args) {
        System.out.println("main creating new Cupboard()");
        new Cupboard();
        System.out.println("main creating new Cupboard()");
        new Cupboard();
        table.f2(1);
        cupboard.f3(1);
    }
    
    static Table table = new Table();
    static Cupboard cupboard = new Cupboard();
}

输出:

在这里插入图片描述

Bowl 类展示类的创建,而 TableCupboard 在它们的类定义中包含 Bowl 类型的静态数据成员。注意,在静态数据成员定义之前,Cupboard 类中先定义了一个 Bowl 类型的非静态成员 b3

由输出可见,静态初始化只有在必要时刻才会进行。如果不创建 Table 对象,也不引用 Table.bowl1Table.bowl2,那么静态的 Bowl 类对象 bowl1bowl2 永远不会被创建。只有在第一个 Table 对象被创建(或被访问)时,它们才会被初始化。此后,静态对象不会再次被初始化。

初始化的顺序先是静态对象(如果它们之前没有被初始化的话),然后是非静态对象,从输出中可以看出。要执行 main() 方法,必须加载 StaticInitialization 类,它的静态属性 tablecupboard 随后被初始化,这会导致它们对应的类也被加载,而由于它们都包含静态的 Bowl 对象,所以 Bowl 类也会被加载。因此,在这个特殊的程序中,所有的类都会在 main() 方法之前被加载。实际情况通常并非如此,因为在典型的程序中,不会像本例中所示的那样,将所有事物通过 static 联系起来。

概括一下创建对象的过程,假设有个名为 Dog 的类:

  1. 即使没有显式地使用 static 关键字,构造器实际上也是静态方法。所以,当首次创建 Dog 类型的对象或是首次访问 Dog 类的静态方法或属性时,Java 解释器必须在类路径中查找,以定位 Dog.class
  2. 当加载完 Dog.class 后(后面会学到,这将创建一个 Class 对象),有关静态初始化的所有动作都会执行。因此,静态初始化只会在首次加载 Class 对象时初始化一次。
  3. 当用 new Dog() 创建对象时,首先会在堆上为 Dog 对象分配足够的存储空间。
  4. 分配的存储空间首先会被清零,即会将 Dog 对象中的所有基本类型数据设置为默认值(数字会被置为 0,布尔型和字符型也相同),引用被置为 null
  5. 执行所有出现在字段定义处的初始化动作。
  6. 执行构造器。你将会在"复用"这一章看到,这可能会牵涉到很多动作,尤其当涉及继承的时候。

显式的静态初始化

你可以将一组静态初始化动作放在类里面一个特殊的"静态子句"(有时叫做静态块)中。像下面这样:

// housekeeping/Spoon.java

public class Spoon {
    static int i;
    
    static {
        i = 47;
    }
}

这看起来像个方法,但实际上它只是一段跟在 static 关键字后面的代码块。与其他静态初始化动作一样,这段代码仅执行一次:当首次创建这个类的对象或首次访问这个类的静态成员(甚至不需要创建该类的对象)时。例如:

// housekeeping/ExplicitStatic.java
// Explicit static initialization with "static" clause

class Cup {
    Cup(int marker) {
        System.out.println("Cup(" + marker + ")");
    }
    
    void f(int marker) {
        System.out.println("f(" + marker + ")");
    }
}

class Cups {
    static Cup cup1;
    static Cup cup2;
    
    static {
        cup1 = new Cup(1);
        cup2 = new Cup(2);
    }
    
    Cups() {
        System.out.println("Cups()");
    }
}

public class ExplicitStatic {
    public static void main(String[] args) {
        System.out.println("Inside main()");
        Cups.cup1.f(99); // [1]
    }
    
    // static Cups cups1 = new Cups(); // [2]
    // static Cups cups2 = new Cups(); // [2]
}

输出:

Inside main
Cup(1)
Cup(2)
f(99)

无论是通过标为 [1] 的行访问静态的 cup1 对象,还是把标为 [1] 的行去掉,让它去运行标为 [2] 的那行代码(去掉 [2] 的注释),Cups 的静态初始化动作都会执行。如果同时注释 [1] 和 [2] 处,那么 Cups 的静态初始化就不会进行。此外,把标为 [2] 处的注释都去掉还是只去掉一个,静态初始化只会执行一次。

非静态实例初始化

Java 提供了被称为_实例初始化_的类似语法,用来初始化每个对象的非静态变量,例如:

// housekeeping/Mugs.java
// Instance initialization

class Mug {
    Mug(int marker) {
        System.out.println("Mug(" + marker + ")");
    }
}

public class Mugs {
    Mug mug1;
    Mug mug2;
    { // [1]
        mug1 = new Mug(1);
        mug2 = new Mug(2);
        System.out.println("mug1 & mug2 initialized");
    }
    
    Mugs() {
        System.out.println("Mugs()");
    }
    
    Mugs(int i) {
        System.out.println("Mugs(int)");
    }
    
    public static void main(String[] args) {
        System.out.println("Inside main()");
        new Mugs();
        System.out.println("new Mugs() completed");
        new Mugs(1);
        System.out.println("new Mugs(1) completed");
    }
}

输出:

在这里插入图片描述

看起来它很像静态代码块,只不过少了 static 关键字。这种语法对于支持"匿名内部类"(参见"内部类"一章)的初始化是必须的,但是你也可以使用它保证某些操作一定会发生,而不管哪个构造器被调用。从输出看出,实例初始化子句是在两个构造器之前执行的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/819150.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

初识Java - 概念与准备

本笔记参考自: 《On Java 中文版》 目录 写在第一行 Java的迭代与发展 Java的迭代 Java的参考文档 对象的概念 抽象 接口 访问权限 复用实现 继承 基类和子类 A是B和A像B 多态 单根层次结构 集合 参数化类型 对象的创建和生命周期 写在第一行 作为一…

Charles抓包工具使用(一)(macOS)

Fiddler抓包 | 竟然有这些骚操作,太神奇了? Fiddler响应拦截数据篡改,实现特殊场景深度测试(一) 利用Fiddler抓包调试工具,实现mock数据特殊场景深度测试(二) 利用Fiddler抓包调试工…

pytorch-gpu 极简安装

1、进入pytoch官网:PyTorch 找到pytorch-gpu版本,看到CUDA11.8、11.7、CPU,这里我选择安装CUDA11.8 2、下载CUDA Toolkit:CUDA Toolkit 11.8 Downloads | NVIDIA Developer 3、下载CUDANN:cuDNN Download | NVIDIA D…

TCP拥塞控制详解 | 1. 概述

网络传输问题本质上是对网络资源的共享和复用问题,因此拥塞控制是网络工程领域的核心问题之一,并且随着互联网和数据中心流量的爆炸式增长,相关算法和机制出现了很多创新,本系列是免费电子书《TCP Congestion Control: A Systems …

基于Spring Boot的鲜花销售网站设计与实现(Java+spring boot+MySQL)

获取源码或者论文请私信博主 演示视频: 基于Spring Boot的鲜花销售网站设计与实现(Javaspring bootMySQL) 使用技术: 前端:html css javascript jQuery ajax thymeleaf 微信小程序 后端:Java springboot…

运算放大器(三):差分放大

一、定义 差分放大电路又称为差动放大电路,当该电路的两个输入端的电压有差别时,输出电压才有变动,因此称为差动。 二、电路结构 常用的差分放大电路如下 图1 所示, 图1 根据虚短和虚断可得:

C# Blazor 学习笔记(0):初识Blazor

文章目录 Blazor是什么适合人群 开始学习BlazorBlazor资源如何创建BlazorBlazor 基础知识介绍文件分布Razor和cshtml的区别Razor介绍 Blazor是什么 Blazor是微软推出的前端框架,有两种形式,以下以Blazor Server为主。具有一下特点 前端是用C#而不是JS前…

Linux编辑器 - vim使用

1.vim的基本概念 Vim是一个广泛使用的文本编辑器,它是在Unix和Linux系统中常用的命令行文本编辑器之一。 vim的主要三种模式 ( 其实有好多模式,目前掌握这 3 种即可 ), 分别是 命令模式 ( command mode )、 插入模式 &#xff0…

仿第八区分发封装打包系统源码教程-轻松打包app

这个是最新的修改优化,优化了一些小bug不能用的问题,具体什么就不讲了,用过的应该有知道的。 大致功能: 支持一键安卓打包,IOS免签、绿标!(几乎媲美原生) 拥有此款神器&#xff0…

AppCompatActivity.setContentView(与activity.setContentView区别)方法解读

AppCompatActivity.setContentView()与Activity.setContentView()主要的区别,Activity.setContentView直接将视图添加到Window上,AppCompatActivity.setContentView()借助AppCompatActivity的Delegate代理类,将要显示的视图加入到代理层视图&…

【网络原理】 (3) (网络层 IP协议 地址管理 路由选择 数据链路层 以太网 MTU 补充:DNS)

文章目录 网络层IP协议地址管理路由选择 数据链路层以太网MTU补充:DNS 网络层 IP协议 网络层的代表,IP协议. 4位版本号(version):指定IP协议的版本,对于IPv4来说,就是4。4位头部长度(header length&…

98. Python基础教程:try...except...finally语句

【目录】 文章目录 1. try...except...finally语法介绍2. try...except...finally执行顺序3. 捕获特定类型的异常4. 捕获所有类型的异常5. 实操练习-打开txt文件并输出文件内容 【正文】 在今天的课程中,我们将学习Python中的异常处理语句try...except...finally。 …

excel英语翻译让你的数据更容易被理解

从前有一个名叫小明的办公室职员,他每天都要处理大量的数据和报表。然而,由于工作需要,他经常收到来自不同国家的Excel表格,这些表格上的内容都是用各种各样的语言编写的,让他很难理解其中的意思。这时,小明…

Qt Creator 11 开放源码集成开发环境新增集成终端和 GitHub Copilot 支持

导读Qt 项目今天发布了 Qt Creator 11,这是一款开源、免费、跨平台 IDE(集成开发环境)软件的最新稳定版本,适用于 GNU/Linux、macOS 和 Windows 平台。 Qt Creator 11 的亮点包括支持标签、多外壳、颜色和字体的集成终端模拟器&am…

建模教程:如何利用3ds Max 和 After Effects 实现多通道渲染和后期合成

推荐: NSDT场景编辑器助你快速搭建可二次开发的3D应用场景 1. 创建基本场景 步骤 1 打开 3ds Max。在 透视视口。 打开 3ds Max 步骤 2 做一个茶壶,放在飞机上。 制作茶壶 步骤 3 我在场景中应用了几个灯光。我选择了光线追踪阴影作为阴影。 光线追…

走进人工智能| 智能物联网 AIoT的魅力交织

前言: AIIoT是指人工智能(AI)与物联网(IoT)的结合。智能物联网是一种技术体系,通过连接和集成物理设备、传感器和互联网,实现设备之间的智能交互和数据共享,为人们提供智能化、自动化…

赛车游戏——【极品飞车】(内含源码inscode在线运行)

前言 「作者主页」:雪碧有白泡泡 「个人网站」:雪碧的个人网站 「推荐专栏」: ★java一站式服务 ★ ★前端炫酷代码分享 ★ ★ uniapp-从构建到提升★ ★ 从0到英雄,vue成神之路★ ★ 解决算法,一个专栏就够了★ ★ 架…

IEEE 802.11——无线局域网的重要里程碑

概要 无线局域网(Wireless Local Area Network,WLAN)已经成为现代生活中不可或缺的一部分,它为我们提供了便捷的无线网络连接,让我们能够在家中、办公室、公共场所等地轻松上网。在无线局域网技术的发展过程中&#x…

【C++】模板进阶(模板的特化,非类型模板参数,模板的分离编译)

文章目录 一、模板使用时一定要加typename的情况二、 非类型模板参数三、模板的特化1.函数模板特化2.类模板特化1.全特化:2. 偏特化:1. 部分特化2.参数更一步限制 四、模板的分离编译1.Stack.h2.Stack.cpp(定义)3.test.cpp 一、模板使用时一定要加typena…

【taro react】---- 获取元素的位置和宽高等信息

1. 需求分析 添加节点的布局位置的查询请求。相对于显示区域,以像素为单位。其功能类似于 DOM 的 getBoundingClientRect。返回 NodesRef 对应的 SelectorQuery。区分小程序和H5的环境,调用 getBoundingClientRect 获取对应的信息。 2. H5 实现 判断传…