IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> Java基础_1 -> 正文阅读

[Java知识库]Java基础_1


1. 对象相关的知识

1. 对象操纵

  1. 所有的编程语言都会提供抽象的机制,汇编语言是对底层机器的轻微抽象,C语言是对汇编的抽象。但是像C语言这类的语言仍需要程序员在机器模型(解决方案空间)和实际解决的问题模型(问题空间)之间建立起一种关联,这个过程是很繁琐的,同时也会产生其它的各种各样的问题。为了克服以上的困难,面向对象的机制就产生了。我们将问题空间中的元素以及它们在解决方案空间内的表示称作对象;

  2. 所有的编程语言都会操纵存在于内存中的元素,在面向对象的语言中操作的其实就是对象,在C++中是通过指针来操作对象的;

  3. 在Java中也是将存在于内存中的元素和对象的概念关联起来的,通常是使用标识符进行操纵的,标识符只是对真实对象的引用,比如我们可以使用遥控器去操纵电视,在这里遥控器就是引用,电视就是实际的对象。没有电视遥控器也可以单独的存在,当然电视也可以单独的存在。一个引用并不是必然的和一个对象进行关联。下面就是一个 String 类型的引用,用于保存单词或者语句:

    String s;
    

    以上仅仅是创建了一个对String对象的引用,并不是真正的创建了一个对象。由于并没有对变量s进行赋值,所以直接进行使用的话会出现错误。我们可以在创建一个引用的时候就直接进行初始化:

    String s = "abc";
    
  4. 在Java中我们可以使用new关键字来创建一个对象,并将对象和引用关联起来:

    String s = new String("asdf");
    

2. 数据存储

程序在运行时会有以下的5中存储区域用于存储数据:

  1. 寄存器:寄存器是最快的存储区域,它位于CPU的内部,寄存器的数量是十分有限的,所以寄存器是按需分配的,在大多数的情况下我们是没有直接控制寄存器的权限的,同时也无法在自己编写的程序中跟踪寄存器的踪迹;
  2. 栈内存:该区域是内存中的一部分,可以通过栈指针来对栈进行操作,栈指针下移是分配内存,上移是释放内存。Java编译器必须知道保存在栈中的数据的生命周期。在栈内存中存储着对Java对象的引用,但是Java对象本身却是存储在Java堆内存之中的;
  3. 堆内存:该区域是一种通用的内存池,所有的Java对象都会存储于该内存区域中。与栈内存不同的是,Java编译器不需要知道对象必须在堆内存上停留多长时间。如果我们想要创建一个对象,只需要使用一个new即可,当执行代码时虚拟机会自动的在堆中进行内存分配,以上的灵活性是需要代价的,分配和清理堆内存所需要的时间会比栈所需要的相应时间会更长;
  4. 常量存储:常量值通产是直接放在代码之中的,可以考虑将它们存储在只读存储器ROM之中;
  5. 非RAM存储:此类数据在程序运行结束之后仍然会存在,一种是序列化对象,对象会被转换为字节流,该字节流通常会被发送到另一台机器之中;还有一种情况是持久化对象,此类对象会被放置在磁盘上;

3. 数据类型

  1. 有一组数据类型在Java中使用的频率是非常高的,它们不是通过new关键字创建出来的,此类数据类型就是Java中的基本数据类型。通常new出来的对象都是放置在堆内存之中的,在堆内存中存储小并且简单的基本数据类型是不划算的,所以对于基本数据类型Java采用了和C/C++一样的策略。也就是不使用new创建此类变量,而是使用一个自动变量,直接存储值,并放置在栈内存之中,以此来更加高效的分配和释放基本数据类型的对象;

  2. Java规定了每种基本数据类型的内存占用大小。这些大小不会像其它的语言那样随着机器环境的变化而变化,Java是一次编译到处运行的;
    在这里插入图片描述
    所有的数值类型都是有符号的(但是在C语言中是存在无符号数的,无符号数在使用的时候很容易造成错误)。boolean类型的大小没有明确的规定,通常定义为取字面值“true”或“false”。基本类型都有自己对应的包装类型,如果希望在堆里面表示基本类型的数据,就可以使用它们的包装类:

    char c = 'x';
    Character ch = new Character(c);
    

    基本数据类型可以自动地转换为包装类型,包装类型也可以自动地转换为基本数据类型。这就是自动装箱和拆箱:

    Character ch = 'x';
    char c = ch;
    
  3. 在Java中有两种类型的数据可用于高精度的计算,它们分别是BigInteger和BigDecimal。尽管它们大致可以划归为包装类型,但是他们并没有相应的基本类型,能够对int以及float进行的运算对于它们来说也是可以的。只不过是通过调用相应的方法来完成的。为了得到更高的精度,相应的计算速度会下降。

4. 作用域

  1. 大多数的编程语言都有作用域的概念,作用域决定了在该范围内定义的变量名的可见性和声明周期,在C,C++以及Java中作用域是由大括号{}的位置来决定的:

    {
        int x = 12;
        // 仅 x 变量可用
        {
            int q = 96;
            // x 和 q 变量皆可用
        }
        // 仅 x 变量可用
        // 变量 q 不在该作用域内
    }
    

    Java变量只有在其作用域内才是可用的,虽说在C以及C++之中以下的操作是合法的,但是在Java之中却是不合法的:

    {
        int x = 12;
        {
            int x = 96; // Illegal
        }
    }
    

    在以上的例子中Java编译器会告知我们变量x已经被定义过了,不过在C以及C++之中会将一个较大作用域的变量隐藏起来,所以是合法的。

  2. Java对象与基本类型具有不同的生命周期,当我们使用new关键字创建一个Java对象的时候,它的生命周期就会超过它所在的作用域:

    {
        String s = new String("a string");
    } 
    // 作用域终点
    

    在以上的代码块之中,引用s在作用域的终点就已经结束了。但是引用s指向的字符仍然占据着内存。new出来的对象会一直存在下去,虽说这样在C++之中可能会造成很多的问题,但是在Java之中我们不需要担心这个问题,因为Java虚拟机之中存在一种叫做垃圾收集器的内存管理单元,垃圾收集器会自动地将我们不再需要的对象回收掉,因此我们在大多数的时候不需要担心内存泄漏的问题;

5. 类的创建

  1. 类型:类型可以决定某一类对象的外观和行为,Java中是使用class关键字来描述一种新的对象的:
    class TypeName {
    	// 这里是类的内部
    }
    
    在以上的代码片之中我们引入了一种新的类型,可以使用new关键字来创建一个这种类型的对象,如下所示:
    TypeName a = new TypeName();
    
  2. 字段:我们可以在类里面存放两种类型的元素:方法和字段,类的字段可以是基本类型,也可以引用类型,如果是引用类型的话,那么就必须初始化该引用将其关联到一个实际的对象上。每一个对象都有用来存储其字段的空间。通常字段在对象之间是不共享的;
    class TypeName {
    	// 以下为一些字段
        int i;
        double d;
        boolean b;
    }
    
    我们可以使用new关键字创建一个名为type的对象引用,我们可以通过该引用来初始化字段值:
    type.i = 47;
    type.d = 1.1;
    type.b = false;
    
    我们可以用以上定义字段的方法嵌套许多对象

6. 基本类型的默认值

  • 如果类的成员变量(也就是字段)是基本数据类型,那么在初始化的时候它们会被赋予一个初始值,这些默认的初始值会在Java初始化类的时候被赋予,这确保了基本类型的字段始终能够被初始化,但是我们最好显示初始化我们的变量以满足我们的需求;
    在这里插入图片描述
    但是局部变量并不会被赋值为默认值,局部变量局势那些不属于以类的字段的变量,例如在方法中定义一个变量 int x;那么这里的变量x并不会被初始化为0,所以在使用x之前我们应该主动地为x赋值。如果我们没有为x初始化,在Java中会直接向我们报一个编译时错误,以说明该变量我们还没有初始化,但是在C++之中我们只会收到一个警告。

7. 命名可见性

命名控制在任何的一门语言之中都是一个重要的问题,因为我们需要处理各个模块之中出现相同命名的问题,以区分不同模块之中的相同命名。并防止两个名称发生冲突。Java使用的方法是为一个类库生成一个明确的名称,Java建议我们反向使用自己的网络域名来为自己的类库命名,一个类库中的类名字必须是唯一的,比如说com.java.test,以上的命名也称为包名,整个包名都是小写的;如果我们想要使用其它包中的类,那么直接使用import语句进行导入就可以了,比如说

import java.util.ArrayList;
// 或
import java.util.*;

8. static关键字

当我们想要满足以下的需求时,就可以使用static关键字:

  1. 只想为特定的字段(也称属性,域)分配一个共享存储空间。而不去考虑究竟要创建多少对象,或者根本就不创建对象;
  2. 创建一个与此类的任何对象都无关的方法,也就是说即使没有创建对象,也能够调用该方法。

当我们说某个字段或方法是静态的时候,就意味着该字段或方法不依赖于任何对象的实例,即使我们并没有创建任何的对象,也可以调用一个类中的静态方法或者访问其中的静态字段。但是对于一个普通的非静态方法和字段,我们必须先创建一个对象并使用创建出来的对象来访问字段或者是方法,因为非静态的字段和方法必须与特定的对象关联。有时候也将静态字段称作类数据,将静态方法称作类方法。我们可以通过在字段或者是方法前面添加static关键字来表示一个静态字段或静态方法:

class StaticTest {
    static int i = 47;
}

现在即使我们创建了两个StaticTest对象,静态变量i也仅仅只用一份的存储空间。我们既可以通过 st1.i 来引用变量 i ,也可以直接使用类名而不创建对象来引用静态变量:StaticTest.i++ ;以上的指令会将 i 的结果增加为48,此时 st1.i 以及st2.i 的值都变为了48。建议使用类名直接引用字段 i ,它强调了变量的静态属性。相比于非静态对象,static关键字只是改变了数据的创建方式。

注:在命令行之中可以使用javac proc.java编译程序,编译通过的话可以使用java proc运行程序。

2. 初始化

1. 利用构造器保证初始化

  1. 在Java中,类的编写者会通过构造器保证每个对象都能够被初始化。如果一个类有构造器,那么Java就可以保证在该类被使用之前自动调用对象的构造器方法,从而保证初始化。为了防止命名冲突并让编译器知道构造器方法的名字,从而自动地调用构造器,Java规定构造器的名字和类的名字保持一致。一下展示了构造器的使用方式以及在我们使用new关键字创建一个对象的时候会发生什么:

    public class Rabbit {
        Rabbit() {
            System.out.print("Rabbit ");
        }
    
        public static void main(String[] args) {
            for (int i = 0; i < 9; i++) {
                new Rabbit();
            }
        }
    }
    

    在这里插入图片描述

    我们可以看到,但我们使用在主方法之中使用new关键字创建一个对象的时候,相应的内存会被分配,构造器会被自动地调用,它保证了我们在使用对象之前进行了正确的初始化;当然构造器也可以传入参数:

    public class Rabbit {
        Rabbit(int i) {
            System.out.print("Rabbit" + i + " ");
        }
    
        public static void main(String[] args) {
            for (int i = 0; i < 9; i++) {
                new Rabbit(i);
            }
        }
    }
    

    运行结果为:

    Rabbit0 Rabbit1 Rabbit2 Rabbit3 Rabbit4 Rabbit5 Rabbit6 Rabbit7 Rabbit8 
    Process finished with exit code 0
    

    构造器使得代码的易读性更高。在Java中对象的创建和初始化是统一的概念,二者不可分割;

  2. 构造器并没有返回值,它是一种特殊的方法,new表达式虽然返回了刚创建的对象的引用,但是构造器本身是不存在返回值的。如果构造器存在返回值并且允许我们选择让它返回什么,那么编译器就还得知道接下来该怎么处理构造器的返回值,并且会带来不必要的混乱。

2. 方法重载

1. 什么是方法重载

  1. 方法的重载是指允许方法具有相同的方法名但是接受的参数是不同的;

  2. 大多数的编程语言要求为每一个方法提供一个独一无二的标识符,每一个函数名都必须是不同的;但是在Java中方法的重载是必要的,因为Java规定构造器方法的名字必须和类的名字是一样的,但是我们又想要通过不同的方式创建一个对象。比如我们想创建一个类,而该类的初始化方法有两种,一种是标准初始化方式,还有一种是带有附加信息的初始化方式。那么我们就需要两种构造器方法了。所以说在Java之中重载是必要的,重载不仅可以应用于构造器,也可以很方便的对其它的方法进行重载;下例中展示了构造器的重载和普通方法的重载:

    public class Rabbit {
        int weight;
    
        Rabbit() {
            System.out.print("Rabbit0.");
            weight = 0;
        }
    
        Rabbit(int weight) {
            this.weight = weight;
            System.out.print("Rabbit1.");
        }
    
        void info() {
            System.out.println("weight = " + weight);
        }
    
        void info(String s) {
            System.out.println(s + weight);
        }
    
        public static void main(String[] args) {
            new Rabbit().info();
            new Rabbit(9).info("weight = ");
        }
    }
    

    运行结果为:

    Rabbit0.weight = 0
    Rabbit1.weight = 9
    

    可以看到我们可以选择不同的方式初始化对象,也可以选择不同的方式展示对象的信息;这一切在Java中都是合法的;

2. 区分重载方法

  1. Java为了区分同名的各个方法,规定每个重载的方法必须有独一无二的参数列表,以此来区分同名的各个方法;甚至仅仅是参数的顺序不同也是被允许的。但是不能够根据返回值的不同来区分同名的方法,例如有以下的两个方法:

    void f(){}
    int f() {return 1;}
    

    虽说在某些情况下编译器可以很轻松的从上下文之中推断出该调用哪一个方法,比如 int x = f(); 但是我们有时候会调用一个方法但是忽略掉它的返回值,只是希望利用方法完成某一些任务,此时我们就会直接调用 f(); ,在这种情况下编译器就无法确定我们调用的究竟是哪一个方法,所以说不能够根据返回类型的不同来区分同名的方法。

  2. 基本类型可以自动地从较小的类型转换为较大的类型,这对重载是适用的,如果比如同名的几个方法中并没有char参数的,但是有同名的int和long参数的,那么当我们向该方法中传入char时它会自动地转换为int(在int存在的情况下并不会转换为long,但是如果只有参数为long的方法的话char也是会转换为long的)。但是如果传入的参数类型大于方法所期望的参数类型,那么就必须先进行向下转换,不然的话编译器会报错。

3. 无参构造器

  1. 无参构造器就是一个不接收参数的构造器,用来创建一个默认的对象。如果我们编写的类中没有构造器,那么编译器就会自动创建一个无参构造器。例如当Rabbit类中没有显式地编写构造器的时候,编译器会自动地添加一个无参构造器,当我们使用new关键字创建一个对象的时候,编译器使用的就是默认的无参构造器。但是一旦我们显式地定义了一个构造器,无论是有参的还是无参的,编译器都不会再自动地创建无参构造器;比如我们在Rabbit类中已经拥有了一个有参构造器,那么当我们使用 new Rabbit(); 创建一个Rabbit对象的时候,编译器就会提示我们找不打匹配的构造器,除非我们自己再创建一个无参构造器。

4. this关键字

  1. 对于两个相同类型的对象a和b,我们可以在Java中直接调用这两个对象的某一个方法:

    class Rabbit {
    	void eat(int day) {
    		/*...*/
    	}
    }
    
    public class RaisingRabbit {
    	public static void main(String[] args) {
    		Rabbit a = new Rabbit(), b = new Rabbit();
    		a.eat(8);
    		b.eat(9);
    	}
    }
    

    就像上面所示的那样,只有一个方法eat(),编译器为了确定调用的到底是aeat()方法还是beat()方法,会在底层做一些工作,可以说eat()方法被隐式地传递了一个参数,它是一个指向操作对象的引用,所以我们可以没有顾虑地直接进行方法的调用。上述例子中的方法调用可以理解为以下的形式:

    Rabbit.eat(a, 8);
    Rabbit.eat(b, 9);
    

    以上的形式是编译器在内部实现的,我们不可以直接编写以上的代码,编译器是不会接受的,但是它确实说明了编译器在底层到底做了什么,从而是我们可以直接进行方法的调用。

  2. 假设我们现在想在方法的内部获得对当前对象的引用,但是对象的引用是被隐式地传达给了编译器,并不在参数列表之中。为了解决以上的问题,Java提供了叫做this的关键字,this关键字只能在非静态方法内部使用。this会生成当前对象的引用,该引用和我们常见的引用的使用方式是一样的。如果在一个类的方法中调用同一个类的其它方法,是不需要使用this关键字的,直接进行调用就可以了。this会被自动地添加。

    public class Rabbit {
        void eat() {
            /*...*/
        }
    
        void drink() {
            eat();
            /*...*/
        }
    }
    

    在 drink 方法之中我们可以使用 this.eat() 来调用 eat 方法,但是这是没有必要的,因为编译器会自动的完成该工作,this 关键字只会用在一些必须显式使用当前对象的特殊场合,例如用在 return 语句之中返回对当前对象的引用:

    public class Rabbit {
        int count = 0;
    
        Rabbit increment() {
            count++;
            return this;
        }
    
        void print() {
            System.out.println("count = " + count);
        }
    
        public static void main(String[] args) {
            final Rabbit rabbit = new Rabbit();
            rabbit.increment().increment().increment().print();
        }
    }
    

    输出

    count = 3
    

    因为 increment() 通过this关键字将当前对象返回,所以说我们可以进行链式编程,如果返回的不是当前的对象而是 void 的话,就不能进行链式编程,如下所示:

    public class Rabbit {
        int count = 0;
    
        void increment() {
            count++;
        }
    
        void print() {
            System.out.println("count = " + count);
        }
    
        public static void main(String[] args) {
            final Rabbit rabbit = new Rabbit();
            rabbit.increment();
            rabbit.increment();
            rabbit.increment();
            rabbit.print();
        }
    }
    

    虽说最终的效果是一样的,但是其在编写代码方面更加的繁琐。当然,this 关键字也可以向其它方法传递当前对象的引用

  3. 在构造器中调用构造器:如果我们的类中有多个构造器,有时候可能会需要在一个构造器之中调用另一个构造器来避免代码的重复,此时我们可以通过 this 关键字实现以上的需求。this 本身生成对当前对象的引用,当我们给 this 一个参数列表的时候,它就会去寻找一个与本身的参数列表相匹配的构造器并调用那个构造器,找不到的话就会报错:

    public class Rabbit {
        int count;
        String s;
    
        public Rabbit(int count, String s) {
            this.count = count;
            this.s = s;
        }
    
        public Rabbit() {
            this(42, "carrot");
        }
    
        public void print() {
            System.out.println("There are " + count + " rabbits.And they eat " + s + ".");
        }
    
        public static void main(String[] args) {
            final Rabbit rabbit = new Rabbit();
            rabbit.print();
        }
    }
    

    输出:

    There are 42 rabbits.And they eat carrot.
    

    以上的例子展示了在无参构造器之中利用带参数列表的 this 来调用与参数列表相匹配的有参构造器。同时也展示了 this 的另外一种用法:在有参构造器之中的变量名 count 和 s 和我们的成员变量的名字是一样的,我们可以通过 this.count 来指明该 count 是成员变量,而不是参数列表中的 count ,以此来避免混淆。

  4. static 的含义:因为静态的方法等都是为类而创建的,不需要任何的对象,所以 static 方法之中不会存在 this。虽说在Java之中是不允许全局方法的,但是静态方法就像是全局方法一样,静态方法可以访问其它的静态方法和静态属性,但是不可以调用非静态方法(非静态方法倒是可以调用静态方法)。如果在我们的代码之中出现了大量的static方法,除非是必要的,不然的话我们就要考虑我们的设计是不是合理的了。

5. 成员初始化

  1. Java会尽量保证所有的变量在使用之前都能够的到恰当的初始化。对于方法中的局部变量,Java本身并不会为它们进行默认地初始化,所以当我们没有显示地对局部变量进行初始化的时候,编译器就会报错,如下所示:

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

    运行以上的代码的话我们会收到一条来自编译器的错误信息,告诉我们 i 可能尚未初始化(在IDEA中编写代码的时候IDEA就会告诉我们需要对 i 进行初始化);

  2. 如果是类成员变量并且成员变量是基本类型,类会保证它自己的的每个基本类型数据成员都有一个初始值,如果是一个对象引用成员变量的话,就会被赋值为null:

    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();
        }
    }
    

    输出:

    Data type Initial value
    boolean false
    char[NUL]
    byte 0
    short 0
    int 0
    long 0
    float 0.0
    double 0.0
    reference null
    
  3. 指定初始化:为一个变量赋初值的一种很直接的方式就是在定义类成员变量的地方直接为其进行赋值,如下所示:

    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;
    }
    

    也可以使用同样的方式初始化非基本类型的对象,比如一个类对象,直接使用 new 关键字进行初始化就可以了;对于一个引用对象,如果不赋值直接使用的话就会出现运行时错误,编译器会提醒我们发生了一个异常。

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

    class Window {
        Window(int marker) {
            System.out.println("Window(" + marker + ")");
        }
    }
    
    class House {
        Window w1 = new Window(1);
    
        House() {
            System.out.println("House()");
            w3 = new Window(33);
        }
    
        Window w2 = new Window(2);
    
        void f() {
            System.out.println("f()");
        }
    
        Window w3 = new Window(3);
    }
    
    public class OrderOfInitialization {
        public static void main(String[] args) {
            House h = new House();
            h.f();
        }
    }
    

    在以上的例子之中可以看到w1,w2,w3三个变量在House的构造器被调用之前就已经被初始化了,而w3会被赋值两次,一次是在构造器被调用之前,还有一次是在构造器调用期间,第一次引用的对象将会被丢弃并作为垃圾被回收;

  5. 静态数据初始化:无论创建多少个对象,静态数据都只会占用单独的一份存储区域。static关键字不能作用域局部变量(因为局部变量都是存户在栈中的,会有多份,不符合静态数据只有一份的特点),所以static关键字只能作用于属性(字段,域)。静态字段的默认初始化值和非静态字段是一样的。下面的例子展示了静态数据的初始化:

    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(1)
    Bowl(2)
    Table()
    f1(1)
    Bowl(4)
    Bowl(5)
    Bowl(3)
    Cupboard()
    f1(2)
    main creating new Cupboard()
    Bowl(3)
    Cupboard()
    f1(2)
    main creating new Cupboard()
    Bowl(3)
    Cupboard()
    f1(2)
    f2(1)
    f3(1)
    

    由输出可以看出静态初始化只有在必要的时候才会进行,如果不创建Table对象,同时也不引用Table.bowl1以及Table.bowl2,那么静态的bowl1以及bowl2永远也不会被创建,只有在Table对象在第一次被创建或者是被引用的时候,其中的静态对象才会得到初始化,并且在此之后静态的对象不会被再次初始化。初始化的顺序先是静态对象,然后是非静态对象。以下是一个创建对象的过程,假设有一个名为Rabbit的类,那么初始化的过程将是这样的:

    1. 即使没有显式地使用static关键字,构造器实际上也是静态方法,所以当首次创建Rabbit类型的对象或是首次访问Rabbit类的静态方法或属性时,Java解释器必须在类路径中查找并定位出Rabbit.class的位置;
    2. 当加载完Rabbit.class之后,有关静态初始化的所有动作都会被执行,所以说静态初始化只会在加载Class对象的时候初始化一次;
    3. 当使用new关键字创建一个Rabbit对象的时候,首先会在堆上为Rabbit对象分配足够的存储空间;
    4. 所分配的空间首先会被清零,也就是将Rabbit对象中的基本类型数据都设置为默认值,引用设置为null;
    5. 执行所有出现在字段定义处的初始化动作;
    6. 最后才是执行构造器;
  6. 显式静态初始化:我们可以将一组静态初始化动作放在一个类中的静态代码块之中:

    public class Rabbit {
        static int count;
        
        static {
            count = 47;
        }
    }
    

    静态代码块之中的初始化和其它的静态初始化动作是一样的,只有在其所在的类被首次创建的时候或者是其中的静态成员被首次访问的时候才会被初始化一次:

    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]
        }
    }
    

    输出为:

    Inside main()
    Cup(1)
    Cup(2)
    f(99)
    
  7. 非静态实例初始化:Java提供了一种叫做实例初始化的语法,用来初始化每个对象的非静态变量,例如:

    class Mug {
        Mug(int marker) {
            System.out.println("Mug(" + marker + ")");
        }
    }
    
    public class Mugs {
        Mug mug1;
        Mug mug2;
        {
            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");
        }
    }
    

    代码块之中的代码会首先被执行,之后才会调用Mugs的构造器,多次执行new Mugs() 的时候代码块之中的代码也会跟着被执行多次,但是如果是静态代码块,那么就只会在第一次时执行一次。使用代码块可以保证不管哪一个构造器被调用的时候,代码块中的操作都会被执行。

  8. 数组初始化:当我们定义一个数组的时候,我们所拥有的只是数组的一个引用,在Java中可以将一个数组赋值给另一个数组,但是只是复制了一个引用,结果就是二者指向同一个真是的数组:

    public class ArraysOfPrimitives {
        public static void main(String[] args) {
            int[] a1 = {1, 2, 3, 4, 5};
            int[] a2;
            a2 = a1;
            for (int i = 0; i < a2.length; i++) {
                a2[i] += 1;
            }
            for (int i = 0; i < a1.length; i++) {
                System.out.println("a1[" + i + "] = " + a1[i]);
            }
        }
    }
    

    输出为:

    a1[0] = 2;
    a1[1] = 3;
    a1[2] = 4;
    a1[3] = 5;
    a1[4] = 6;
    

    同时,所有的数组(无论是对象数组还是基本类型数组)都有一个固定成员length,它会告诉我们数组中一共有多少个元素,我们不能对其进行修改。

6. 可变参数列表

  1. 可变参数列表可以应用在参数个数或者是类型未知的场合。由于所有的类都继承自Object,所以我们可以创建一个Object数组作为参数,具体的做法如下所示:

    class A {}
    
    public class VarArgs {
        static void printArray(Object[] args) {
            for (Object obj: args) {
                System.out.print(obj + " ");
            }
            System.out.println();
        }
        
        public static void main(String[] args) {
            printArray(new Object[] {47, (float) 3.14, 11.11});
            printArray(new Object[] {"one", "two", "three"});
            printArray(new Object[] {new A(), new A(), new A()});
        }
    }
    

    输出:

    47 3.14 11.11 
    one two three 
    A@15db9742 A@6d06d69c A@7852e922
    

    printArray()函数的参数是Object数组,这样我们就实现了可变参数列表,但是以上的方式实在是太笨拙了,所以在 Java 5 之后添加了可变参数列表的特性,我们可以使用以下的方式来令一个方法可以接受可变的参数列表:

    public class NewVarArgs {
        static void printArray(Object... args) {
            for (Object obj: args) {
                System.out.print(obj + " ");
            }
            System.out.println();
        }
        
        public static void main(String[] args) {
            printArray(47, (float) 3.14, 11.11);
            printArray(47, 3.14F, 11.11);
            printArray("one", "two", "three");
            printArray(new A(), new A(), new A());
            printArray((Object[]) new Integer[] {1, 2, 3, 4});
            printArray(); // Empty list is OK
        }
    }
    

    输出:

    47 3.14 11.11 
    47 3.14 11.11 
    one two three 
    A@15db9742 A@6d06d69c A@7852e922 
    1 2 3 4 
    

    当我们指定参数之后,编译器实际上会自动地为我们填充数组,所以我们获取的仍然是一个数组,printArray()函数也就可以使用 for-in 进行迭代遍历了。
    程序的最后一行表明可变参数的个数可以是0,这可以应用于可选的尾随参数:

    public class OptionalTrailingArguments {
        static void f(int required, String... trailing) {
            System.out.print("required: " + required + " ");
            for (String s: trailing) {
                System.out.print(s + " ");
            }
            System.out.println();
        }
        
        public static void main(String[] args) {
            f(1, "one");
            f(2, "two", "three");
            f(0);
        }
    }
    

    输出:

    required: 1 one 
    required: 2 two three 
    required: 0 
    

    以下的例子中展示了非Object类型的可变参数列表,以及可变参数列表转换为数组的情形,如果类表中没有任何的元素,那么会转变为大小为0的数组:

    public class VarargType {
        static void f(Character... args) {
            System.out.print(args.getClass());
            System.out.println(" length " + args.length);
        }
        
        static void g(int... args) {
            System.out.print(args.getClass());
            System.out.println(" length " + args.length)
        }
        
        public static void main(String[] args) {
            f('a');
            f();
            g(1);
            g();
            System.out.println("int[]: "+ new int[0].getClass());
        }
    }
    

    输出为:

    class [Ljava.lang.Character; length 1
    class [Ljava.lang.Character; length 0
    class [I length 1
    class [I length 0
    int[]: class [I
    

    可变参数列表和自动装箱是可以搭配使用的:

    public class AutoboxingVarargs {
        public static void f(Integer... args) {
            for (Integer i: args) {
                System.out.print(i + " ");
            }
            System.out.println();
        }
        
        public static void main(String[] args) {
            f(1, 2);
            f(4, 5, 6, 7, 8, 9);
            f(10, 11, 12);
        }
    }
    

    可变参数列表方法的重载可能会导致错误的发生:

    public class OverloadingVarargs {
        static void f(Character... args) {
            System.out.print("first");
            for (Character c: args) {
                System.out.print(" " + c);
            }
            System.out.println();
        }
        
        static void f(Integer... args) {
            System.out.print("second");
            for (Integer i: args) {
                System.out.print(" " + i);
            }
            System.out.println();
        }
        
        static void f(Long... args) {
            System.out.println("third");
        }
        
        public static void main(String[] args) {
            f('a', 'b', 'c');
            f(1);
            f(2, 1);
            f(0);
            f(0L);
            //- f(); // Won's compile -- ambiguous
        }
    }
    

    输出:

    first a b c
    second 1
    second 2 1
    second 0
    third
    

    注意被注释掉的最后一行,如果我们调用不带参数的 f() ,那么编译器就不会知道应该调用哪一个方法,最终会导致错误的发生。

3. 封装

1. 介绍

  1. 在我们完成代码的编写之后,如果过一段时间之后我们可能会发现更好的实现方式,所以会选择重写之前的代码,重写之前的代码的过程叫做重构,使之前的代码可读性更高且更加的便于维护。但是在修改和完善代码的过程之中也是有很多要考虑的因素的,其中一各因素就是客户端程序员并不希望在我们修改源代码值之后他们所编写的代码也需要跟着修改。以上的考虑对于类库的设计者是非常的重要的,当类库中的代码升级为新版本之后,类库的使用者并不需要修改他们自己的代码,而类库的设计者也必须保留有修改代码的权利。
  2. 对以上的问题的考虑是比较的复杂的,比如类库的设计者如何知道哪一些属性是被客户端程序员所使用的;以及如果类库的实现者想要删除旧的实现来添加新的实现所造成的结果将是怎样的。任何成员的改动都可能会破坏客户端程序员的代码,这对于类库的开发者而言是一个很大的束缚;
  3. 为了解决以上的问题,Java提供了访问修饰符,供类库的开发者来指明那些对于客户端的程序员是可用的,而哪些对于客户端的程序员是不可用的。访问修饰符根据对访问权限的限制程度依次为:public,protected,包访问权限(没有关键字)和private。
  4. Java也提供了包的概念,一个类是在相同的包下面还是在不同的包下是会影响到访问修饰符的;

2. 包的概念

  1. 一个包里面包含一组类,它被组织在一个单独的命名空间之中,如果我们想要使用一个包中的某一个类,那么直接使用 import 语句进行导入就可以了:

    import java.util.ArrayList;
    
    public class SingleImport {
        public static void main(String[] args) {
            ArrayList list = new ArrayList();
        }
    }
    

    之所以使用导入,是为了提供一种管理命名空间的机制,所有的类名之间都是相互隔离的。类A中的方法f()不会与类B中的具有相同签名的方法f()发生冲突。同时类名冲突也是有可能的,为了解决类名冲突,我们会为每一个类创建唯一的标识符组合。其实当我们故意的创建了一个包名和类名都和JDK中的某个类相同的类的时候,JVM其实也是可以分辨出来的,具体是怎么做的在JVM总结之中会详细说明。

  2. 一个Java源代码文件称为一个编译单元,每一个编译单元文件的后缀名必须是.java。在编译单元之中可以有一个public类,它的类名必须与文件名是相同的。每个编译单元只能有一个public类,否则编译器是不会接受的。如果在一个编译单元之中还有其它的类,那么在包之外是无法访问到那些类的,因为它们不是public类,它们为主public类提供支持类;

  3. 代码组织:当编译一个.java文件的时候,.java文件中的每一个类都会产生一个输出文件,并且每一个输出文件的名字都和.java中的类的名字是相同的,只是后缀名是.class。所以在编译少量的.java文件之后,会得到大量的.class文件。对于像C语言这样的编译型语言,编译之后会产生一个中间文件,然后与链接器和类库生成器产生的其它同类文件打包在一起。但是Java不是这么做的,在Java中可执行程序是一组.class文件,它们可以打包压缩成一个Java文档文件(JAR,使用jar文档生成器)。Java解释器会负责查找,加载和解释得到的文件。

  4. 类库就是一组类文件,每一个源文件之中通常都包含一个 public 类和任意数量的非 public 类,因此每个文件都有一个 public 组件,可以通过 package 语句将很多的组件集中在一起。

3. 访问权限修饰符

  1. 包访问权限:默认的访问权限没有关键字,通常被称为包访问权限。当前包中的所有其它类都可以访问默认访问权限的成员,当然包之外的类就不能够访问它了。由于一个编译单元只能隶属于一个包,所以同一个编译单元之中的类是可以相互访问的。
  2. public访问权限:被public修饰的类,属性或者是方法对于所有人来说都是可见的;
  3. private访问权限:被private修饰的类成员,除了包含给类成员的类之外,其它的任何类都无法访问其中的成员,使用private,类库编写者可以自由地修改那个被修饰的成员而无须担心会影响同一个包下的其它类。private一个用途就是控制如何创建对象,比如可以防止别人直接访问某个特定的构造器;
  4. protected访问权限:相同包内的其它类可以访问protected修饰的元素,同时基类的子类也可以访问基类中protected修饰的元素;
  5. 当我们定义一个包访问权限的类时,可以在类中定义一个 public 构造器,编译器并不会报错,但是在实际上该构造器在包的外部仍然是无法被访问的,它的访问权限并没有真正的变为public的。

4. 类访问权限

  1. 类不能是private的,也不能是protected的,所以对于类的访问权限就只有包访问权限或者是public。如果想要防止类被外界访问,可以将所有的构造器声明为private,这样就只有我们自己能够创建对象(在类的static成员中完成这个操作,因为static成员不需要创建类就可以被访问):
    class Soup1 {
        private Soup1() {}
        
        public static Soup1 makeSoup() { // [1]
            return new Soup1();
        }
    }
    
    class Soup2 {
        private Soup2() {}
        
        private static Soup2 ps1 = new Soup2(); // [2]
        
        public static Soup2 access() {
            return ps1;
        }
        
        public void f() {}
    }
    // Only one public class allowed per file:
    public class Lunch {
        void testPrivate() {
            // Can't do this! Private constructor:
            //- Soup1 soup = new Soup1();
        }
        
        void testStatic() {
            Soup1 soup = Soup1.makeSoup();
        }
        
        void testSingleton() {
            Soup2.access().f();
        }
    }
    
    可以像[1]那样通过static方法创建对象,也可以像[2]那样先创建一个静态对象,然后在用户访问它时返回对象的引用就可以了。Soup1以及Soup2展示了如何通过将所有的构造器都声明为private的以防止直接创建某个类的对象。Soup2使用了单例模式(但是它是不安全并且是不完美的,具体的单例模式会在设计模式之中讲解到),单例模式只允许创建类的一个对象,由于Soup2类的对象是作为Soup2的 private static 成员而创建的,所以说有且只有一个,我们只能通过public修饰的access()方法访问到以上的对象。

4. 复用

1. 简介

  1. 代码复用是非常重要的,程序员在工作的过程之中总是编写重复的代码是不现实的,复用有时候只是简单地复制代码,但是这么做的效果并不是很好。Java是通过类来实现代码的复用的。程序员可以直接使用别人构建或者是调试过的代码而不是自己创建一个新类重现开始。
  2. 有以下的两种方式来使用现存的代码,一种是组合,即直接在新类中创建现有类的对象,通过这种方式可以复用现有代码的功能。还有一种方式叫做继承,该方式会创建现有类类型的新类;

2. 组合

  1. 其实组合是非常常见的,如果想要使用一个类的话直接将相应类对象的引用放置在一个新的类中就可以了:

    public class Rabbit {
        private Carrot carrot = new Carrot();
    
        @Override
        public String toString() {
            return "Rabbit { " +
                    "carrot = " + carrot +
                    " }";
        }
    
        public static void main(String[] args) {
            final Rabbit rabbit = new Rabbit();
            System.out.println(rabbit);
        }
    }
    
    class Carrot {
        private String s;
        Carrot() {
            System.out.println("Carrot()");
            s = "Constructed";
        }
    
        @Override
        public String toString() {
            return s;
        }
    }
    

    输出:

    Carrot()
    Rabbit { carrot = Constructed }
    

    编译器并不会为引用创建一个默认对象,因为这会带来不必要的开销。因此需要我们自己进行初始化,初始化引用有以下的四种方式:

    • 在对象被定义的时候,也就是说在调用构造函数之前进行初始化;
    • 在构造函数中进行初始化;
    • 在实际使用对象的时候进行初始化,这种初始化方式通常被称为延迟初始化,在对象创建的开销比较大并且并不需要每次都创建对象的情况下该方式是非常实用的;
    • 使用实例对引用进行初始化;

    以下展示了四种创建方式的例子:

    public class Rabbit {
        private String s1 = "Rabbit_1"; // 在对象被定义的时候进行初始化
        private String s2;
        private String s3;
        private Carrot carrot;
        private float f;
        private int i;
    
        public Rabbit() {
            System.out.println("Inside Rabbit()");
            // 在类的构造器之中进行初始化
            s2 = "Rabbit_2";
            f = 3.14f;
            carrot = new Carrot();
        }
    
        // 使用实例进行初始化
        {i = 42;}
    
        @Override
        public String toString() {
            // 在实际使用对象的时候进行初始化
            if (s3 == null) s3 = "Rabbit_3";
            return "Rabbit{" +
                    "s1='" + s1 + '\'' +
                    ", s2='" + s2 + '\'' +
                    ", s3='" + s3 + '\'' +
                    ", carrot=" + carrot +
                    ", f=" + f +
                    ", i=" + i +
                    '}';
        }
    
        public static void main(String[] args) {
            final Rabbit rabbit = new Rabbit();
            System.out.println(rabbit);
        }
    }
    
    class Carrot {
        private String s;
        Carrot() {
            System.out.println("Carrot()");
            s = "Constructed";
        }
    
        @Override
        public String toString() {
            return s;
        }
    }
    

    输出:

    Inside Rabbit()
    Carrot()
    Rabbit{s1='Rabbit_1', s2='Rabbit_2', s3='Rabbit_3', carrot=Constructed, f=3.14, i=42}
    

3. 继承

  1. 继承是Java中的一个非常重要的语法,在我们创建一个新类的时候即使没有显式地使用继承语法,该类也会自动地继承Java的标准根类对象Object;

  2. 在声明一个类的时候,可以使用extends关键字继承一个类,然后新类就会自动地获取基类中所有的字段和方法;对于新类来说,既可以重写基类中的方法,也可以添加新的方法,重写方法之后如果想要调用基类中的方法的话可以使用super关键字:

    class Cleanser {
      private String s = "Cleanser";
      public void append(String a) { s += a; }
      public void dilute() { append(" dilute()"); }
      public void apply() { append(" apply()"); }
      public void scrub() { append(" scrub()"); }
      @Override
      public String toString() { return s; }
      public static void main(String[] args) {
        Cleanser x = new Cleanser();
        x.dilute(); x.apply(); x.scrub();
        System.out.println(x);
      }
    }
    
    public class Detergent extends Cleanser {
      // Change a method:
      @Override
      public void scrub() {
        append(" Detergent.scrub()");
        super.scrub(); // Call base-class version
      }
      // Add methods to the interface:
      public void foam() { append(" foam()"); }
      // Test the new class:
      public static void main(String[] args) {
        Detergent x = new Detergent();
        x.dilute();
        x.apply();
        x.scrub();
        x.foam();
        System.out.println(x);
        System.out.println("Testing base class:");
        Cleanser.main(args);
      }
    }
    /* Output:
    Cleanser dilute() apply() Detergent.scrub() scrub() foam()
    Testing base class:
    Cleanser dilute() apply() scrub()
    */
    

    我们可以为每一个类都创建一个main方法,而且即使类只是包访问权限,也可以访问main,此时其他包中的子类只能访问该类中的公共部分;除此之外,protected成员也允许派生类进行访问;

  3. 当我们创建一个派生类的时候,从外部来看派生类与基类具有相同的接口,但是继承并不是简单地复制了基类的接口,当创建了一个派生类的对象的时候,该对象包含有基类的子对象,这个子对象和我们直接显式地创建的基类对象是一样的,该子对象被包装在派生类的对象之中;在继承体系之中我们必须正确的初始化基类子对象,并且只有一种方式可以正确的初始化基类的子对象,那就是使用基类的构造器方法,Java会自动的在派生类之中插入对基类构造函数的调用,以下展示了三个层次的调用结构:

    class Art {
      Art() {
        System.out.println("Art constructor");
      }
    }
    
    class Drawing extends Art {
      Drawing() {
        System.out.println("Drawing constructor");
      }
    }
    
    public class Cartoon extends Drawing {
      public Cartoon() {
        System.out.println("Cartoon constructor");
      }
      public static void main(String[] args) {
        Cartoon x = new Cartoon();
      }
    }
    /* Output:
    Art constructor
    Drawing constructor
    Cartoon constructor
    */
    

    构造会从基类开始进行,所以说基类会在派生类使用它之前得到正确的初始化;但是如果我们在基类之中显式的声明了有参的构造函数,那么在基类之中就不会存在无参构造函数,所以必须在派生类之中显式地调用基类的有参构造函数,不然的话编译器就会报错说找不到基类的构造函数。此外对基类构造函数的调用必须在子类构造函数中的第一行,否则的话编译器也会报错:

    public class Animal {
        Animal(int i) {
            System.out.println("Animal constructor " + i);
        }
    }
    
    class Cat extends Animal {
        Cat(int i) {
            super(i);
            System.out.println("Cat constructor " + i);
        }
    }
    
    class Hawksbill extends Cat {
        Hawksbill(int i) {
            super(i);
            System.out.println("Hawksbill constructor " + i);
        }
    
        public static void main(String[] args) {
            new Hawksbill(42);
        }
    }
    /* Output:
    	Animal constructor 42
    	Cat constructor 42
    	Hawksbill constructor 42
    */
    
  4. 还有一种重用的方式是委托,Java并不直接支持委托。委托是介于继承和组合之间的一种形式,因为会将一个成员对象放置在正在构建的类中(比如组合),同时在新类中公开来自成员对象的所有方法(比如继承),例如我们定义一个宇宙飞船的控制模块类:

    public class SpaceShipControls {
      void up(int velocity) {}
      void down(int velocity) {}
      void left(int velocity) {}
      void right(int velocity) {}
      void forward(int velocity) {}
      void back(int velocity) {}
      void turboBoost() {}
    }
    

    可以使用继承来复用以上的代码:

    public class DerivedSpaceShip extends SpaceShipControls {
      private String name;
      public DerivedSpaceShip(String name) {
        this.name = name;
      }
      @Override
      public String toString() { return name; }
      public static void main(String[] args) {
        DerivedSpaceShip protector =
            new DerivedSpaceShip("NSEA Protector");
        protector.forward(100);
      }
    }
    

    可以使用以下的方式将方法转发到底层控制对象,所以接口与继承的接口是一样的:

    public class SpaceShipDelegation {
      private String name;
      private SpaceShipControls controls =
        new SpaceShipControls();
      public SpaceShipDelegation(String name) {
        this.name = name;
      }
      // Delegated methods:
      public void back(int velocity) {
        controls.back(velocity);
      }
      public void down(int velocity) {
        controls.down(velocity);
      }
      public void forward(int velocity) {
        controls.forward(velocity);
      }
      public void left(int velocity) {
        controls.left(velocity);
      }
      public void right(int velocity) {
        controls.right(velocity);
      }
      public void turboBoost() {
        controls.turboBoost();
      }
      public void up(int velocity) {
        controls.up(velocity);
      }
      public static void main(String[] args) {
        SpaceShipDelegation protector =
          new SpaceShipDelegation("NSEA Protector");
        protector.forward(100);
      }
    }
    
  5. 如果在基类之中一个方法被多次的重载,则当我们在派生类之中重载该方法也会是可以的,所有的重载都会正常的工作:

    class Homer {
      char doh(char c) {
        System.out.println("doh(char)");
        return 'd';
      }
      float doh(float f) {
        System.out.println("doh(float)");
        return 1.0f;
      }
    }
    
    class Milhouse {}
    
    class Bart extends Homer {
      void doh(Milhouse m) {
        System.out.println("doh(Milhouse)");
      }
    }
    
    public class Hide {
      public static void main(String[] args) {
        Bart b = new Bart();
        b.doh(1);
        b.doh('x');
        b.doh(1.0f);
        b.doh(new Milhouse());
      }
    }
    /* Output:
    doh(float)
    doh(char)
    doh(float)
    doh(Milhouse)
    */
    

    但是比起重载,更常见的是使用 @Override 注解进行方法的重写,该注解会保证我们进行正确的重写,而不是重载等;

  6. 当我们想要在一个新类之中包含一个已有类的功能的时候,使用组合而不是继承,而且在新类中嵌入的对象通常是私有的;当我们想要使用一个现有类并发出它的新版本的时候,通常会使用继承。“是一个(is-a)”的关系使用继承来表达的,而“有一个(has-a)”的关系是用组合来表达的。

  7. 继承是新类与基类的一种关系,该关系可以表述为新类是已有类的一种类型;在以下的代码之中有一个基类Instrument和一个派生类Wind,编译器会将Wind对象也作为一种类型的Intrument:

    // reuse/Wind.java
    // Inheritance & upcasting
    class Instrument {
        public void play() {}
        
        static void tune(Instrument i) {
            // ...
            i.play();
        }
    }
    
    // Wind objects are instruments
    // because they have the same interface:
    public class Wind extends Instrument {
        public static void main(String[] args) {
            Wind flute = new Wind();
            Instrument.tune(flute); // Upcasting
        }
    }
    

    tune方法的参数是一个Instrument类型的引用,但是在Wind的main()方法之中传递给tune()方法的却是一个Wind的引用。但是编译器会保证最终的结果是正确的,编译器会把Wind引用转换为Instrument引用,这种转换称作向上转型。向上转型是一个更具体的类转换为一个更一般的类,所以说向上转型是安全的。也就是说派生类是基类的一个超集,他可能比基类包含更多的方法,但是必须包含基类中的方法。也可以进行向下转型,但是通常是会有问题的。

  8. 虽说继承在我们学习Java的过程中是被反复强调的,但是这并不意味着我们应该尽可能的使用它,相反的是我们应该尽可能的不使用继承,而是使用组合,除非是必须使用继承才能够满足我们的代码需求。

4. final关键字

  1. 虽说final关键字的具体含义会因为上下文环境而有些微的不同,但是通常都是指“不可改变的”。以下讨论了可能使用final的三个地方,分别是数据,方法和类。

  2. 恒定不变的值可能是永远都不会改变的编译时常量,也可能是一个在运行时初始化就不会改变的值,编译时常量会在编译的时候计算,减少了运行时的负担。在Java中,编译时常量必须是基本类型的,而且要使用final关键字进行修饰,并且在定义常量的时候就进行初始化。一个被static关键字以及final关键字同时修饰的属性只会占用一段不能改变的存储空间。当使用final修饰一个对象的引用而不是基本类型的对象的话,final只会保证所使用的引用是恒定不变的,一旦引用被指向了某个对象,它就不可能再去指向其它的对象,但是对象本身是可以改变的,Java并没有提供任何的将对象设置为常量的方法。

    import java.util.*;
    
    class Value {
        int i; // package access
        
        Value(int i) {
            this.i = i;
        }
    }
    
    public class FinalData {
        private static Random rand = new Random(47);
        private String id;
        
        public FinalData(String id) {
            this.id = id;
        }
        // Can be compile-time constants:
        private final int valueOne = 9;
        private static final int VALUE_TWO = 99;
        // Typical public constant:
        public static final int VALUE_THREE = 39;
        // Cannot be compile-time constants:
        private final int i4 = rand.nextInt(20);
        static final int INT_5 = rand.nextInt(20);
        private Value v1 = new Value(11);
        private final Value v2 = new Value(22);
        private static final Value VAL_3 = new Value(33);
        // Arrays:
        private final int[] a = {1, 2, 3, 4, 5, 6};
        
        @Override
        public String toString() {
            return id + ": " + "i4 = " + i4 + ", INT_5 = " + INT_5;
        }
        
        public static void main(String[] args) {
            FinalData fd1 = new FinalData("fd1");
            //- fd1.valueOne++; // Error: can't change value
            fd1.v2.i++; // Object isn't constant
            fd1.v1 = new Value(9); // OK -- not final
            for (int i = 0; i < fd1.a.length; i++) {
                fd1.a[i]++; // Object isn't constant
            }
            //- fd1.v2 = new Value(0); // Error: Can't
            //- fd1.VAL_3 = new Value(1); // change reference
            //- fd1.a = new int[3];
            System.out.println(fd1);
            System.out.println("Creating new FinalData");
            FinalData fd2 = new FinalData("fd2");
            System.out.println(fd1);
            System.out.println(fd2);
        }
    }
    

    输出为:

    fd1: i4 = 15, INT_5 = 18
    Creating new FinalData
    fd1: i4 = 15, INT_5 = 18
    fd2: i4 = 13, INT_5 = 18
    

    带有恒定初始值的 static final 基本变量,即编译时常量的命名全部使用大写,单词之间使用下划线进行分割;我们不能够因为某一个数据是final修饰的就认为其在编译期就可以确定它的值,比如以上例子中的 i4 和 INT_5 就是在运行时才会被赋值为一个随机数。同时 i4 没有用static关键字进行赋值,但是 INT_5 使用了static关键字,所以说 INT_5 的值并不会因为创建了第二个 FinalData 对象而改变,因为 static 修饰的属性在加载时已经被初始化了,而不是每次创建新对象的时候都会被初始化。上述代码中对数组的内容进行修改是允许的,不可变的仅仅是对数组的引用。

  3. 如果在定义一个 final 属性的时候没有对其进行赋值操作,那么就必须在构造器之中对其进行赋值,否则的话编译器就会报错,总之需要通过在定义时进行赋值或者是在构造器之中进行赋值来保证 final 属性在使用之前已经被初始化:

    class Poppet {
        private int i;
        
        Poppet(int ii) {
            i = ii;
        }
    }
    
    public class BlankFinal {
        private final int i = 0; // Initialized final
        private final int j; // Blank final
        private final Poppet p; // Blank final reference
        // Blank finals MUST be initialized in constructor
        public BlankFinal() {
            j = 1; // Initialize blank final
            p = new Poppet(1); // Init blank final reference
        }
        
        public BlankFinal(int x) {
            j = x; // Initialize blank final
            p = new Poppet(x); // Init blank final reference
        }
        
        public static void main(String[] args) {
            new BlankFinal();
            new BlankFinal(47);
        }
    }
    
  4. 在方法的参数列表中将参数声明为 final 的话,那么当方法内部使用参数的时候不能够对参数的值进行修改:

    class Gizmo {
        public void spin() {
            
        }
    }
    
    public class FinalArguments {
        void with(final Gizmo g) {
            //-g = new Gizmo(); // Illegal -- g is final
        }
        
        void without(Gizmo g) {
            g = new Gizmo(); // OK -- g is not final
            g.spin();
        }
        
        //void f(final int i) { i++; } // Can't change
        // You can only read from a final primitive
        int g(final int i) {
            return i + 1;
        }
        
        public static void main(String[] args) {
            FinalArguments bf = new FinalArguments();
            bf.without(null);
            bf.with(null);
        }
    }
    
  5. 对于一个 private 的方法来说,其被隐式地指定为 final(也可以添加final关键字,但是得到的效果是一样的),因为 private 方法是不能被重写的,如果在一个派生类之中声明了一个一模一样的方法的话,也仅仅是恰好拥有相同的命名而已,如果将同名的新方法声明为 public,protected或者是包访问权限的,那么得到的效果并不是重写,而仅仅是创建了一个新的方法而已,并且如果强行添加一个 @Override 注解的话,编译器会报错:

// It only looks like you can override
// a private or private final method
class WithFinals {
    // Identical to "private" alone:
    private final void f() {
        System.out.println("WithFinals.f()");
    }
    // Also automatically "final":
    private void g() {
        System.out.println("WithFinals.g()");
    }
}

class OverridingPrivate extends WithFinals {
    private final void f() {
        System.out.println("OverridingPrivate.f()");
    }
    
    private void g() {
        System.out.println("OverridingPrivate.g()");
    }
}

class OverridingPrivate2 extends OverridingPrivate {
    public final void f() {
        System.out.println("OverridingPrivate2.f()");
    } 
    
    public void g() {
        System.out.println("OverridingPrivate2.g()");
    }
}

public class FinalOverridingIllusion {
    public static void main(String[] args) {
        OverridingPrivate2 op2 = new OverridingPrivate2();
        op2.f();
        op2.g();
        // You can upcast:
        OverridingPrivate op = op2;
        // But you can't call the methods:
        //- op.f();
        //- op.g();
        // Same here:
        WithFinals wf = op2;
        //- wf.f();
        //- wf.g();
    }
}
  1. 如果在类声明为final的话,就意味着该类是不能被继承的,这么做是因为该类的设计就是永远不需要改动的,或者是出于安全的考虑不希望它有子类,在 final 类中的方法加上final修饰符的话并不会增加任何的意义:
    // Making an entire class final
    class SmallBrain {}
    
    final class Dinosaur {
        int i = 7;
        int j = 1;
        SmallBrain x = new SmallBrain();
        
        void f() {}
    }
    
    //- class Further extends Dinosaur {}
    // error: Cannot extend final class 'Dinosaur'
    public class Jurassic {
        public static void main(String[] args) {
            Dinosaur n = new Dinosaur();
            n.f();
            n.i = 40;
            n.j++;
        }
    }
    
  2. 使用 final 是需要非常小心的,除非自己确定 final 修饰的属性在今后的使用过程中确实没有被修改的需求,有时候一个方法对于一个客户端程序员来说可能是需要重写的,但是却将其声明为了 final ,也就是说方法的编写者并没有意识到该方法可能确实会被重写。Java类库之中的某些类是很好的例子,比如Java 1.0/1.1 的Vector类,它被广泛的使用,设计该类的程序员处于效率的考虑将其中的方法全都声明为 final 的,实际上效率并没有因此而得到提升,而且如果不将其声明为 final 的话可能会更有用。不过在现代的Java中使用ArrayList取代了以往的Vector,以弥补当初设计的不足。

5. 类初始化和加载

  1. 在Java中,只有在类的代码首次被使用的时候才会进行加载,通常是指创建类的第一个对象或者是访问了类的static属性或方法。构造器其实也是一个static方法,只不过它的static关键字是隐式的。所以可以说当一个类的任意一个static成员被访问的时候,它就会被加载。所有的static对象和static代码块在加载时会按照文本的顺序依次被初始化,并且static变量只会被初始化一次;
  2. 以下的例子展示了类初始和加载的过程:
    // The full process of initialization
    class Insect {
        private int i = 9;
        protected int j;
        
        Insect() {
            System.out.println("i = " + i + ", j = " + j);
            j = 39;
        }
        
        private static int x1 = printInit("static Insect.x1 initialized");
        
        static int printInit(String s) {
            System.out.println(s);
            return 47;
        }
    }
    
    public class Beetle extends Insect {
        private int k = printInit("Beetle.k.initialized");
        
        public Beetle() {
            System.out.println("k = " + k);
            System.out.println("j = " + j);
        }
        
        private static int x2 = printInit("static Beetle.x2 initialized");
        
        public static void main(String[] args) {
            System.out.println("Beetle constructor");
            Beetle b = new Beetle();
        }
    }
    
    输出:
    static Insect.x1 initialized
    static Beetle.x2 initialized
    Beetle constructor
    i = 9, j = 0
    Beetle.k initialized
    k = 47
    j = 39
    
    当在命令行中执行java Beetle的时候,首先会去访问Beetle类的main()方法,它是一个静态方法,接下来加载器会找出Beetle类的编译代码,在加载的过程中编译器会注意到基类的存在,于是会继续加载基类,不管是否会创建基类对象,基类都会被加载,直到加载到根基类。然后根基类的static初始化会开始执行,接着是派生类的static初始化,派生类的static初始化可能会依赖于基类的static初始化。至此必要的类都加载完毕,可以进行对象的创建了。首先对象中的所有基本类型变量都会被置为默认值,对象引用会被置为null。接着调用基类的构造器,当基类构造器完成后,派生类的实例变量会按照文本顺序初始化,最终构造器的剩余部分会被执行。
  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2021-08-18 12:34:18  更:2021-08-18 12:35:27 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/21 4:15:51-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码