Java Programming Review 01

Java Review

Java 的特性 write once run everywhere(一次书写,任何地方都可运行)。

Java 的工作方式

在这里插入图片描述

根据上图,我们可以知道,我们要做的就是编写源代码文件,用 javac 编译程序把文件编译 (Compile) 为在某个虚拟机上运行的字节码。

在这里插入图片描述

Java 的程序结构

需要明确源文件 (Source File),类 (Class) 和方法 (Method) 之间的关系。

在这里插入图片描述

在 Java 中所有东西都会属于某个类。我们会建立源文件 (.java),然后将他编译为新的类文件 (.class)。真正被执行的是类。不管程序有多大(不管有多少类),一定会有一个 main() 作为程序的起点。一个程序只要一个 main() 作为运行

在这里插入图片描述

这里需要注意实际编程中对于源文件 (.java) 的命名。我们之前说过,一个源文件 (.java) 可以包含多个类 (class)。但是,其中用 public 修饰的 “公共类 (public class)” 最多只能有一个,而源文件 (.java) 的文件名必须和这个 “公共类 (public class)” 保持一致。如果所有类 (class) 都没有修饰符 (etc. public, private…),那么源文件 (.java) 的名称随意。

  1. Java 保存的文件名必须与类名一致;
  2. 如果文件中只有一个类,文件名必须与类名一致;
  3. 一个 Java 文件中只能有一个 public 类;
  4. 如果文件中不止一个类,文件名必须与 public 类名一致;
  5. 如果文件中不止一个类,而且没有 public 类,文件名可与任一类名一致

类与对象

Java 是面向对象的编程语言,对象是靠类的模型塑造出来的。 根据对象的组成部分,我们在构建类 (class) 的时候,就会有以下对应:

  • 对象的已知的事物 - 实例变量 (instance variable)
  • 对象会执行的动作 - 方法 (method)

在这里插入图片描述

实例变量 (instance variable)对象本身已知的事物。它们代表对象的状态(数据),且同类型的所有对象 都应独立拥有一份该类型的数据
方法 (method)对象可执行的动作叫作方法。在设计类时,也需要设计出操作该对象的方法。

我们需要明确的是,对象 ≠ 类。类 (class) 是用来创建对象 (object/instance) 的模型。 换言之,类是对象的蓝图,它会告诉虚拟机创建出什么类型的对象,根据某个类创建出的对象都会拥有自己的实例变量。比如,我们可以用按钮类 (button class) 来创建出许多不同颜色、大小、文字的按钮对象。

创建对象

为了测试和运用对象,一般来说我们至少要创建 2 个类。一个是要被操作于对象的类(比如 Dog, Television 等);一个是用于测试该类的类(比如 DogTestDrive, TVTestDrive 等),这个类会包含一个 main()。

在这里插入图片描述

对于用类 (class) 创建出的对象,我们使用圆点运算符 (.) 来存取对象的变量以及调用方法:

在这里插入图片描述

再次强调,Java 程序是由一组类 (class) 组成的,其中有一个类 (class) 会带有启动用的 main() 方法。

代码实例:GameLauncher & GuessGame & Player

变量(primitive 主数据类型和引用)

primitive 主数据类型变量

和 C 一样,Java 中的变量也要先声明后使用。声明变量的规则也相同:Must have a type & Must have a name。下面来看 Java 中的 primitive 主数据类型有哪些:

在这里插入图片描述
在这里插入图片描述

值得注意的是 Java 中的 char 占用 2 Bytes (16 bits),比 C 中大了一倍,这是因为 Java 中使用的是 Unicode 编码

具体的变量赋值方法和 C 也保持一致,只需注意变量类型的大小进行赋值。

在这里插入图片描述

对于类 (class) 和变量 (variable) 的命名,我们需要遵循以下规则:

  1. 名称必须以字母、下划线、或 $ 开头,不能使用数字开头
  2. 除第一个字符以外,后续位置都可使用数字
  3. 避开 Java 的保留字

引用变量(对象引用)

实际上不存在 “对象变量”,只有引用 (reference) 到对象的变量(可以看作是一个专项遥控器)。对象引用变量保存的是存取对象的方法,所以我们需要格外注意,此时,对象本身并没有被放进变量中:

在这里插入图片描述

对象的声明、创建和赋值有 3 个步骤:

在这里插入图片描述

因此,引用变量本身有多大,我们是不知道的,但是 primitive 主数据类型变量有多大是可知的。而对于 JAVA 虚拟机来说,所有的引用大小都一样。另外,我们无法对引用变量进行运算。而没有引用到任何对象的引用变量,其值为 null

数组(Array)

“数组犹如杯架”。数组也是对象,这一点从声明、创建和赋值数组上也能清楚地看出来

在这里插入图片描述

需要注意的是,数组中的元素可以是引用变量 (reference variable)。比如我们为 Dog() 对象创建一个数组:

在这里插入图片描述

在这里插入图片描述

对象的行为

重申,“类所描述的是对象知道什么 (实例变量) 和执行什么 (方法)”。同一类型的对象能有不同的方法行为吗?答案是肯定的。我们知道同一类的对象都会有相同的方法,但是方法可以根据实例变量的值来表现不同的行为。

比如:Song 这个类有 title 和 artist 两个实例变量以及 play() 这个方法。在调用某个实例的 play() 方法时,就会播放不同作品,因为 play() 方法如下所示:

在这里插入图片描述

除此以外,我们也可选择给方法 (method) 传递参数。和所有编程语言定义函数一样,这就需要我们 “在定义方法是声明形参 (parameter),在调用时传递实参 (argument)”

在这里插入图片描述

和其他所有编程语言一样,我们可以选择传入多个参数、以变量的形式传递参数等。另外和 C 一样,当我们能在定义方法时声明了返回类型,那么就一定要在方法终止时,返回对应类型的数据/变量。另外值得注意的一点在于,Java 是通过拷贝传递参数的。换言之,方法不会改变传入的变量本身,只是使用了它的值。

运用参数和返回类型

在设计一个类的时候,一般可以把方法 (method) 分为两类:getter 和 setter。getter 的工作很简单,就是为了返回实例变量;而 getter 则是用来设定实例变量的值。但在这个时候就需要考虑另一个问题:封装 (Ensapsulation).

我们在之前提过,可以使用圆点运算符 (.) 来访问甚至修改对象的实例变量,但是为了安全性考虑,我们希望只能通过 setter() 来进行设定而避免使用圆点运算符来进行直接存取。这就需要使用存取修饰符 (access modifier),我们使用这个修饰符来定义封装的基本原则:

将实例变量标记为私有 (private),并提供公有 (public) 的 setter 和 getter 来控制存取动作。

声明与初始化实例变量

我们在设计类 (class) 的时候,需要声明实例变量的类型和名字,但是不一定需要初始化实例变量 (赋一个初始值)这是因为实例变量永远都会有默认值

在这里插入图片描述

这里我们需要明确实例变量和局部变量之间的差异:

实例变量局部变量
声明在 class 中声明在 method 中
使用前未必初始化,有默认值使用前必须初始化,没有默认值

在 Java 中, == 运算符用来比较两个 primitive 主数据类型,或者判断两个引用 (reference) 是否引用同一个对象。使用 equals() 来两个对象是否在意义上相等。

在这里插入图片描述

Java 编程 Tips

在设计代码时,要时刻牢记,Java 是面向对象的编程语言,因此,我们专注于程序中出现的事物(对象)而不是过程。

在这里插入图片描述

注意 Java 里的 ++ 和 – 运算符。和 C 基本一致,++x 是先运算后使用,x++ 是先使用后运算。

For 循环分为基础班和加强版:

基础版:

在这里插入图片描述

加强版:

在这里插入图片描述

实际上可以看作用 :代替了 in 语句。在其他语言中,这也被称为 For each 语句

Java 函数库

ArrayList 类:

在这里插入图片描述

这是一个可以满足我们需要一个变长数组梦想的类。需要注意的是,ArrayList 中只能加入对象 (object) 参数,另外,我们需要格外注意 ArrayList 和普通数组 (array) 之间的区别。我们无法对数组 (array) 使用圆点运算符 (.) 调用方法,比如:我们没法从数组中删除元素,一般来说我们只能从数组中调用元素。最多只能用存取它的 length 实例变量。在使用 ArrayList 时,我们实际上在使用 ArrayList 类型的对象,就和所有一般的对象一样,我们可以使用圆点运算符来调用它的方法 (method)。

在这里插入图片描述

在这里插入图片描述

Java 中表示逻辑运算的 &&(与)和 ||(或)运算为短运算符,意味着编译器只要知道符号左边的逻辑表达式不成立,就不会再继续运行。& 和 | 作为长运算符强制要求虚拟机运算符号两边的表达式,不过一般用于位运算 (bit calculation)。

再 Java 的 API 中,类 (class) 是包含在包 (package) 中的。Java 函数库中的每个类都属于某个包,因此想要使用时一定要指明函数库类的完整名称:包的名称 + 类的名称。 比如使用 ArrayList 时,我们先用 import 指明了包的名称,所以 ArrayList 的全程应该是:java.util.ArrayList。当然我们在编程时,也可每次都将全名显式地打出来,但这一般只会在作为参数或是声明返回类型时使用。

继承与多态

继承 (Inherit)

用一张图展现继承:

在这里插入图片描述

子类在继承父类的方法的同时,也可以去覆盖父类原有的方法,从而生成独属于自己的方法。因此,现在当我们进行程序设计时,就可以将继承纳入考量范围,此时,就可以进行如下步骤:

  1. 找出具有共同属性和行为的对象
  2. 设计代表共同状态与行为的类
  3. 决定子类是否要让某些行为有不同的运作方式
  4. 通过寻找使用共同行为的子类来找出更多抽象化的机会
  5. 完成类的层次继承

比如最直观的例子,动物图谱:

在这里插入图片描述

当我们创建了一个对象,并调用其引用的方法时,会调用到与该对象类型最接近的方法。

在这里插入图片描述

我们可以使用 IS-A 测试来检验继承关系是否合适,比如 “老虎是动物”,但是我们不能说 “浴盆是浴室”。同样,对于层次化的继承关系,若类 Y 是继承类 X,且类 Y 是类 X 的父类,那么 Z 应该能够通过 IS-A X 类测试。

父类也可以通过设置存取权限 (access level) 决定子类是否能继承某些特定的成员。我们主要关注 4 种存取权限:

private > default > protected > public

受限制程度从左向右递减。在子类继承父类时,public 会被继承,而 private 不会被继承。

在这里插入图片描述

多态(Polymorphsim)

“当我们定义出一组类的父类时,可以用子类的任何类来填补任何需要和期待父类的位置”

我们从声明引用和创建对象的方法来看多态。我们之前在进行对象声明、创建与赋值时有 3 个步骤:

Dog myDog = new Dog();

  1. 声明一个引用变量
  2. 创建对象
  3. 连接引用和对象

此时要求引用类型和对象类型必须保持一致。但是,在使用多态时,可以不一致

在这里插入图片描述

换言之,运用多态时,引用类型可以是对象类型的父类。 比如:

Animal[] animals = new Animal[2];

animals[0] = new Cat();

animals[1] = new Dog();

除此以外,返回对象和参数也可以使用多态。 比如:

在这里插入图片描述

此时,在调用 giveShot() 这个方法时,就可以传递一个 DogCat 等任意 Animal 子类的对象

当使用子类的方法去覆盖父类的方法时,需要遵守一定的规则:

  • 参数必须一样,且返回类型必须要兼容:父类对该方法定义了哪种参数,覆盖此方法的子类就必须拥有同样的参数。不论父类声明的返回类型是什么,子类必须声明返回一样的类型或是该类型的子类
  • 不能降低方法的存取权限:存取权限必须相同或更加开放

方法的重载 (Overload)

重载 (Overload) 的意义在于两个方法的名称相同,但参数不同。相当于为一个方法针对不同的场景创建了一个新的版本。所以,重载版的方法只是恰好有相同名字的不同方法,它与继承和多态无关,和覆盖更完全不是一回事。

  • 返回的类型可以不同
  • 不能只改变返回类型:如果只有返回类型不同,但参数一样,这是万万不可的
  • 可以更改存取权限

深入多态(接口与抽象类)

抽象类 (abstract class)

在实际应用种,我们有时会希望某些类不应该被初始化。比如我们前面使用的例子:我们会去使用 wolf, tiger 等 Animal 的子类,但一般不会去使用一个 Animal 对象。这个时候,我们就需要一种方法来防止某些类被初始化,换言之,让这个类不能被 new 出来。此时就可以将类标记为抽象类 (使用 abstract 标记),此时编译器就能知道,不能创建该类的任何实例。但此时该类仍能作为引用类型,因此 Animal 作为父类被别的类继承的能力并没有消失

所以到目前为止,我们就有了两种类 (class):具体 (concrete) 的类可以被初始化为对象;抽象 (abstract) 的类不能被初始化为对象

在这里插入图片描述

可以看到,我们仍能使用抽象类来声明引用变量给多态使用,但是无法创建对应的对象。简而言之,抽象类除了被继承过之外,是没有用途,没有值,没有目的的

抽象的方法 (abstract method)

抽象的类表示该类一定被 extend 过(继承过),抽象的方法表示该方法一定被覆盖过。既然该方法一定会被覆盖,那么写出这个抽象方法的代码就没有意义,所以抽象方法没有实体(方法体):

在这里插入图片描述

如果我们声明了一个抽象方法,就必须将类标记为抽象类。因为我们不能在非抽象类种有抽象方法。就算只有一个抽象方法,也必须要标记该类为抽象。不管是抽象类 (abstract class) 还是抽象方法 (abstract method),其存在本身就是为了多态服务,我们只是希望使用父类作为方法的参数、返回的类型以及数组的类型

所有的抽象方法必须要被实现。实现抽象方法就如同覆盖方法一般。抽象方法没有内容,它只是为了标记多态而存在,这就是说,在继承树结构下的第一个具体类一定要实现所有的抽象方法。必须以相同的方法名和参数以及相容的返回类型创建出非抽象的方法(用非抽象的方法覆盖抽象的方法)

终极对象 Object

在 Java 中,所有类型都是从 Object 这个类继承出来的。Object 这个类是所有类的源头(父类)。比如在 ArrayList 中,它在定义方法时,就是用 Object 这个类指代所有的类,因此它能够支持任意类的存入和取用。

Object 这个终极类在被定义时,包含了很多方法,这些方法我们平时经常会使用:

在这里插入图片描述

但是也需要考虑到使用 Object 类型作为引用会有的一些问题,举一个简单的例子:

我们创建一个 Dog 类的 ArrayList:

ArrayList <Dog> mgDog = new ArrayList<Dog>();

Dog a = new Dog();

mgDog.add(a);

Dog d = myDog.get(0);

可以看到,此时当我们将 Dog 对象装进 ArrayList<Dog> 时,它会被当作 Dog 来输出和输出。但如果我们想要创建一个可以放入任意类型对象的 ArrayList<Object> 时,就会产生问题。

在这里插入图片描述

可以看到,任何从 ArrayList<Object> 中取用的对象,他都会被当做 Object 类型的引用而不管其原来是什么东西

在这里插入图片描述

类似的情况也会发生在我们将 Object 作为参数以及返回类型的时候。在将 Object 作为参数时,我们可以很顺利地将各种类型地对象传递给方法,但是在作为返回类型时,会将任意类型的对象都转换为 Object 类型。

在这里插入图片描述

我们之前已经知道,对象会带有从父类继承下来的所有东西,这就表示每个对象,除了真正的类型以外,也可以当作时 object 来处理。比如当我们 new Snowboard() ,此时在堆上会有一个 Snowboard 对象之外,它会包含 Object 在其中:
在这里插入图片描述

所谓的多态就意味着 “很多类型”,在这个时候,我们既可以把 Snowboard 当作 Snowboard,也可当作 Object。

我们看回之前那个 ArrayList<Object> 的例子,我们一般希望能够从其中取回的对象仍能变回存入时的类型,而不是 Object 类型,此时可以使用强制转换

接口(Interface)

接口可以帮助我们解决多重继承的问题但又不会产生 “致命方块” 问题。

“致命方块” 是多重继承会带来的问题。用一张图就能简单地理解:

在这里插入图片描述

接口解决 “致命方块” 的手段很简单:把所有的方法 (method) 设置为抽象的 (abstract)。如此一来,子类就必须要实现此方法,因此 Java 虚拟机在执行期间就不会混淆版本。

在这里插入图片描述

这里需要注意 2 点:

1. 在定义接口时,使用 interface 代替 class
2. 在实现接口时,要使用 implement 这个关键词。同时要注意,实现 interface 时必须在某个类的继承之下

在这里插入图片描述

当我们把一个类当作多态类型使用时,相同的类型必然来自同一个继承树,且必须是该多态类型的子类。比如定义为 Canine 的参数可以接受 Dog 与 Wolf 类,但是无法接受 Cat 与 Lion 类。但当我们使用接口作为多态类型时,对象可来自任何地方,唯一的条件就是该对象必须来自实现此接口的类。另外,类可以实现多个接口

在子类调用父类的方法

使用 super 关键词即可:

在这里插入图片描述

Summary

在这里插入图片描述

栈与堆

在 JAVA 运行时,内存中会有两部分区域备受关注:

  1. 堆 (Heap):对象 (Object) 生存的空间
  2. 栈 (Stack):变量 (Variable) 和方法 (Method) 生存的空间

在这里插入图片描述

这里就需要关注一下变量 (Variable) 的生存空间。因为在 JAVA 中,主要有两种变量:实例变量 (Instance) 和局部变量 (Local Variable)。这会直接决定变量的生存空间为何

在这里插入图片描述

当调用方法时,该方法会和它的状态(执行到哪一行)一起被封装在一个堆栈块放在调用栈的顶部。

在这里插入图片描述

如果局部对象是个引用变量 (Reference Variable),它仍和方法 (Method) 一同存在于栈 (stack),只有被引用的对象本身在堆 (heap) 中。

对于实例变量,我们需要关注的更多一些。当我们创建了一个对象,比如 CellPhone() 时,Java 需要在堆 (Heap) 上帮 CellPhone 找到一个位置,并预留足够存放所有实例变量的空间。 如果实例变量时 Primitive 主数据类型,那很简单,只需要根据数据类型预留空间即可;最值得关注的是当该实例变量是一个引用变量的情况,此时 Java 会为留下引用变量而不是对象本身所用的空间。

如果有变量声明而没有给它赋值,那么只会留下变量的空间:

private Nokia nok;

直到引用变量被赋值了一个新的 Nokia 对象,才会在堆 (Heap) 上占有空间:

private Nokia nok = new Nokia();

在这里插入图片描述

构造函数 (Construct Function)

我们回忆一下创建对象的过程:声明 - 创建 - 赋值。这里的 “创建” 实际上是我们使用 new 去调用一个构造函数 (Construct Function) 的过程,这个构造函数带有我们在初始化对象时会执行的代码,如果我们没有自己创建这个构造函数,编译器也会自动生成构造函数在语法上有 2 点需要注意:1. 构造函数没有声明返回类型;2. 一定要与类的名称相同。

构造函数最大的特征在于它可以在对象被赋值给引用变量 (Reference Variable) 之前就执行:

在这里插入图片描述

因此,我们可以借助构造函数,让这个对象在被创建时,就已经有具体的实例变量,而不需要再用 set() 方法依靠用户去为实例变量人工赋值。这么做可以避免用户忘记初始化对象,并且在初始化之前就去使用对象。

在这里插入图片描述

在实际情况中,我们会希望能够为对象赋予一个默认值,具体的效果为:如果我们没有向构造函数传递参数,则以默认值初始化对象,否则使用我们传递的参数进行初始化。但是这在 Java 中会比较麻烦,因为一旦构造函数(或者任意方法)有参数,那么我们在调用时,就必须为它传参,否则无法通过编译。(在 Python 中我们可以在定义函数时,为参数设置默认值,这样在调用时就无需传参

有人或许会考虑使用 if 语句来判断,如果传参为 0/-1 就使用默认值,否则就使用参数。但这种方法并不严谨,因为总会有用户想要初始值为 0/-1 的情况。所在此时,就必须有 2 个构造函数。

在这里插入图片描述

不要奢望编译器会自动为我们考虑周到,一旦我们写了一个有参数的构造函数,且需要另一个无参数的构造函数,那就必须由我们手写出来。如果类由一个以上的构造函数,则参数(顺序/类型)必须不一样!!换言之,必须重载构造函数 (Reload Construct Method)

在这里插入图片描述

Summary

在这里插入图片描述

父类的构造函数

下面我们会结合父类 (Super Class) 来展开进一步的讨论。首先来看继承父类时,在堆 (Heap) 上的空间安排:

在这里插入图片描述

在创建新对象时,所有从父类继承下来的构造函数都会被执行。 因此,执行 new 实际上启动了一个连锁,即便是抽象的类 (abstract class) 也会有构造函数,尽管我们不能对抽象的类执行 new,但是在它的子类创建出实例时,构造函数依旧会被执行。构造函数在被执行的时候,第一件事是去执行它父类的构造函数,直到连锁到 Object 类为止。具体的过程如下:

在这里插入图片描述

可以看到,这里有 2 个类 (class),如果算上 Object 则共有 3 个类 (class)。其中 Hippo 位于继承树末端,此时,执行构造函数的顺序如下:

在这里插入图片描述

当我们手动书写构造函数时,在子类中,会使用 super() 语句来调用父类的构造函数。

对象的生命周期

父类必须在子类创建完成之前完整地成型。父类的构造函数也必须在子类的构造函数结束之前结束。 这是因为 super() 一定是构造函数的第一个语句

当父类的构造函数有参数时,子类可以使用 super(parameter) 将实例变量的值传递给父类

在这里插入图片描述

这是一个具体的例子,Hippo 类继承 Animal 类的方法,其中有 getName() 方法来获取 name 实例变量的值,而 Hippo 本身没有实例变量,他需要以来 Animal 中的 name 来记录 “名字”,因此在运行构造函数时,需要使用 super() 将 name 的值传递给父类 Animal,让他直到该存储什么值

现在考虑怎么在某个构造函数中调用另一个重载版的构造函数。此时需要使用 this() 这个语句。该语句必须在构造函数的第一行,因此,需要在 this()super() 之间做出取舍。this() 的功能是从某构造函数调用同一个类的另一个构造函数。

在这里插入图片描述

对象的生存周期取决于引用变量的生存周期,这又要看该变量是局部变量 or 实例变量。

首先来看局部变量的生存周期:

在这里插入图片描述

局部变量的作用仅限于它存在的方法之中,一旦该方法执行结束,换言之,从栈 (stack) 被弹出,就意味着局部变量 “死亡”。同理,实例变量的存活取决于对象的生死:

在这里插入图片描述

现在来看变量如何影响对象的生命周期。只要还有活着的引用变量,对象就仍存活。但如果对于对象的唯一引用死亡,对象就会被从堆 (Heap) 中踢开

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值