原文:
zh.annas-archive.org/md5/ceefdd89e585c59c20db6a7760dc11f1
译者:飞龙
第九章:学习 Java 方法
随着我们开始逐渐熟悉 Java 编程,在本章中,我们将更仔细地研究方法,因为尽管我们知道可以调用它们来执行它们的代码,但它们比我们迄今讨论的更多。
在本章中,我们将研究以下内容:
-
方法结构
-
方法重载与覆盖
-
一个方法演示迷你应用程序
-
方法如何影响我们的变量
-
方法递归
首先,让我们快速回顾一下方法。
技术要求
您可以在 GitHub 上找到本章中的代码文件github.com/PacktPublishing/Android-Programming-for-Beginners-Third-Edition/tree/main/chapter%2009
。
方法重温
这张图总结了我们对方法的理解:
图 9.1 – 理解方法
正如我们在图中所看到的,关于方法还有一些疑问。我们将彻底揭开方法的面纱,看看它们是如何工作的,以及方法的其他部分到底在做什么,这将在本章后面讨论。在第十章**,面向对象编程和第十一章**,更多面向对象编程中,我们将在讨论面向对象编程时澄清方法的最后几个部分的神秘之处。
究竟什么是 Java 方法?
方法是一组变量、表达式和控制流语句,它们被捆绑在一个以签名为前缀的大括号内。我们已经使用了很多方法,但我们还没有仔细地看过它们。
让我们从方法结构开始。
方法结构
我们编写的方法的第一部分称为签名。这是一个假设的方法签名:
public boolean addContact(boolean isFriend, string name)
如果我们添加一对大括号{}
,并加入一些方法执行的代码,那么我们就有了一个完整的方法——一个定义。这是另一个虚构的但在语法上是正确的方法:
private void setCoordinates(int x, int y){
// code to set coordinates goes here
}
正如我们所见,我们可以从代码的其他部分使用我们的新方法,如下所示:
// I like it here
setCoordinates(4,6);// now I am going off to setCoordinates method
// Phew, I'm back again - code continues here
在我们调用setCoordinates
方法的时候,我们程序的执行将分支到该方法内部包含的代码。该方法将逐步执行其中的所有语句,直到达到结束,然后将控制返回给调用它的代码,或者如果它遇到return
语句,则更早地返回。然后,代码将从方法调用后的第一行继续运行。
这是另一个完整的方法示例,包括使方法返回到调用它的代码的代码:
int addAToB(int a, int b){
int answer = a + b;
return answer;
}
使用前面方法的调用可能如下所示:
int myAnswer = addAToB(2, 4);
显然,我们不需要编写方法来将两个int
变量相加,但这个例子帮助我们更深入地了解方法的工作原理。首先,我们传入值2
和4
。在方法签名中,值2
被赋给int a
,值4
被赋给int b
。
在方法体内,变量a
和b
被相加,并用于初始化新变量int answer
。行return answer
将存储在answer
中的值返回给调用代码,导致myAnswer
被初始化为值6
。
请注意,前面示例中的每个方法签名都有所不同。这是因为 Java 方法签名非常灵活,允许我们精确地构建我们需要的方法。
方法签名确切地定义了方法如何被调用以及方法如何返回值,这值得进一步讨论。
让我们给签名的每个部分起一个名字,这样我们就可以把它分成几部分,并了解这些部分。
以下加粗的文本是一个方法签名,其部分已经标记好以便讨论。另外,看一下接下来的表格,以进一步澄清签名的哪一部分是哪一部分。这将使我们对方法的讨论更加简单明了:
修饰符 | 返回类型 | 方法名称(参数)
修饰符
在我们之前的例子中,我们只在一些例子中使用了修饰符,部分原因是方法不必使用修饰符。修饰符是一种指定代码可以使用(调用)你的方法的方式,通过使用public
和private
等修饰符。
变量也可以有修饰符,如下所示:
// Most code can see me
public int a;
// Code in other classes can't see me
private String secret = "Shhh! I am private";
修饰符(用于方法和变量)是一个重要的 Java 主题,但最好在我们讨论其他重要的 Java 主题时处理,这些主题我们已经在几次中绕过了 - 类。我们将在下一章中这样做。
返回类型
表中的下一个是返回类型。像修饰符一样,返回类型是可选的。所以,让我们仔细看一下。我们已经看到我们的方法可以做任何我们可以用 Java 编码的事情。但是如果我们需要方法所做的结果呢?到目前为止,我们所见过的返回类型的最简单的例子如下:
int addAToB(int a, int b){
int answer = a + b;
return answer;
}
在这里,签名中的返回类型被突出显示。返回类型是int
。addAToB
方法将一个值返回给调用它的代码,这个值将适合在一个int
变量中。
返回类型可以是我们到目前为止所见过的任何 Java 类型。然而,方法不一定要返回一个值。当方法不返回值时,签名必须使用void
关键字作为返回类型。当使用void
关键字时,方法体不得尝试返回一个值,否则会导致编译错误。但是,它可以使用没有值的return
关键字。
以下是一些返回类型和使用return
关键字的有效组合:
void doSomething(){
// our code
// I'm done going back to calling code here
// no return is necessary
}
另一个组合如下:
void doSomethingElse(){
// our code
// I can do this as long as I don't try and add a value
return;
}
以下代码是另一个组合:
void doYetAnotherThing(){
// some code
if(someCondition){
// if someCondition is true returning to calling
code
// before the end of the method body
return;
}
// More code that might or might not get executed
return;
/*
As I'm at the bottom of the method body
and the return type is void, I'm
really not necessary but I suppose I make it
clear that the method is over.
*/
}
String joinTogether(String firstName, String lastName){
return firstName + lastName;
}
我们可以依次调用前面的每个方法,如下所示:
// OK time to call some methods
doSomething();
doSomethingElse();
doYetAnotherThing();
String fullName = joinTogether("Alan ","Turing")
// fullName now = Alan Turing
// continue with code from here
前面的代码将依次执行每个方法中的所有代码。
方法的名称
表中的下一个是方法的名称。当我们设计自己的方法时,方法名是任意的。但是惯例是使用清楚解释方法将要做什么的动词。此外,使用名称的第一个单词的第一个字母小写,后面的单词的第一个字母大写的约定。这被称为驼峰命名法,就像我们在学习变量名时学到的那样。考虑下一个例子:
void XGHHY78802c(){
// code here
}
前面的方法是完全合法的,并且可以工作;然而,让我们看看使用这些约定的三个更清晰的例子:
void doSomeVerySpecificTask(){
// code here
}
void getMyFriendsList(){
// code here
}
void startNewMessage(){
// code here
}
这样更清晰,因为名称明确表明了方法将要做什么。
让我们来看看方法中的参数。
参数
表中的最后一个主题是参数。我们知道方法可以将结果返回给调用代码。但是如果我们需要从调用代码中向方法共享一些数据值呢?
参数允许我们将值发送到被调用的方法中。当我们查看返回类型时,我们已经看到了一个带有参数的例子。我们将看同样的例子,但更仔细地看一下参数:
int addAToB(int a, int b){
int answer = a + b;
return answer;
}
在这里,参数被突出显示。参数包含在括号中,(参数放在这里)
,紧跟在方法名之后。
请注意,在方法体的第一行中,我们使用a + b
,就好像它们已经被声明和初始化为变量一样。那是因为它们是。方法签名的参数是它们的声明,调用方法的代码初始化它们,就像下一行代码中突出显示的那样。
int returnedAnswer = addAToB(10,5);
另外,正如我们在之前的例子中部分看到的,我们不仅仅在参数中使用int
。我们可以使用任何 Java 类型,包括我们自己设计的类型。
此外,我们也可以混合和匹配类型。我们还可以使用尽可能多的参数来解决我们的问题。一个例子可能会有所帮助。
void addToAddressBook(char firstInitial,
String lastName,
String city,
int age){
/*
all the parameters are now living, breathing,
declared and initialized variables.
The code to add details to address book goes here.
*/
}
前面的例子将有四个声明和初始化的变量,准备好使用。
现在我们将看看方法主体——放在方法内部的内容。
主体
在我们之前的例子中,我们一直在伪代码中使用注释来描述我们的方法主体,比如以下的注释:
// code here
还有以下的用法:
// some code
addToAddressBook
方法也被使用了。
/*
all the parameters are now living, breathing,
declared and initialized variables.
The code to add details to address book goes
here.
*/
但我们已经完全知道主体中要做的事情。到目前为止,我们学到的任何 Java 语法都可以在方法的主体中使用。事实上,如果我们回想一下,到目前为止我们在本书中编写的所有代码都已经在一个方法中。
我们接下来可以做的最好的事情是编写一些在主体中执行操作的方法。
使用方法演示应用程序
在这里,我们将快速构建两个应用程序,以进一步探索方法。首先,我们将使用Real World Methods
应用程序探索基础知识,然后我们将一窥新主题,Exploring Method Overloading
应用程序。
像往常一样,您可以以通常的方式打开已经输入的代码文件。下面的两个方法示例可以在第九章文件夹和Real World Methods
和Exploring Method Overloading
子文件夹中的下载包中找到。
真实世界的方法
首先,让我们创建一些简单的工作方法,包括返回类型参数和完全运作的主体。
要开始,创建一个名为Real World Methods
的新 Android 项目,使用MainActivity.java
文件,通过在编辑器上方的MainActivity.java标签上单击左键,我们可以开始编码。
首先,将这三个方法添加到MainActivity
类中。将它们添加到onCreate
方法的闭合大括号}
后面。
String joinThese(String a, String b, String c){
return a + b + c;
}
float getAreaCircle(float radius){
return 3.141f * radius * radius;
}
void changeA(int a){
a++;
}
我们添加的第一个方法叫做joinThese
。它将返回一个String
值,并需要传入三个String
变量。在方法主体中,只有一行代码。return a + b + c
代码将连接传入的三个字符串,并将连接后的字符串作为结果返回。
下一个方法名为getAreaCircle
,接受一个float
变量作为参数,然后也返回一个float
变量。方法的主体简单地使用了圆的面积公式,结合传入的半径,然后将答案返回给调用代码。3.141
末尾的奇怪的f
是为了让编译器知道这个数字是float
类型的。任何浮点数都被假定为double
类型,除非它有尾随的f
。
第三个和最后一个方法是所有方法中最简单的。请注意,它不返回任何东西;它有一个void
返回类型。我们包括了这个方法,以明确一个我们想要记住关于方法的重要观点。但在我们讨论它之前,让我们看看它的实际操作。
现在,在onCreate
方法中,在调用setContentView
方法之后,添加这段代码,调用我们的三个新方法,然后在 logcat 窗口中输出一些文本:
String joinedString = joinThese("Methods ", "are ", "cool ");
Log.i("joinedString = ","" + joinedString);
float area = getAreaCircle(5f);
Log.i("area = ","" + area);
int a = 0;
changeA(a);
Log.i("a = ","" + a);
运行应用程序,查看 logcat 窗口中的输出,这里为您提供方便:
joinedString =: Methods are cool
area =: 78.525
a =: 0
在 logcat 输出中,我们可以看到的第一件事是joinedString
字符串的值。正如预期的那样,它是我们传入joinThese
方法的三个单词的连接。
接下来,我们可以看到getAreaCircle
确实计算并返回了基于传入半径的圆的面积。
a
变量即使在传入changeA
方法后仍保持值0
的事实,值得单独讨论。
发现变量范围
输出的最后一行最有趣:a=: 0
。在onCreate
方法中,我们声明并初始化了int a
为0
,然后调用了changeA
方法。在changeA
的主体中,我们用代码a++
增加了a
。然而,在onCreate
方法中,当我们使用Log.i
方法将a
的值打印到 logcat 窗口时,它仍然是0。
因此,当我们将a
传递给changeA
方法时,实际上传递的是存储在a
中的值,而不是实际变量a
。这在 Java 中被称为按值传递。
提示
当我们在一个方法中声明一个变量时,它只能在该方法中被看到。当我们在另一个方法中声明一个变量时,即使它具有完全相同的名称,它也不是同一个变量。变量只在声明它的方法内部具有作用域。
对于所有基本变量,将它们传递给方法的工作方式是这样的。对于引用变量,它的工作方式略有不同,我们将在下一章中看到。
重要说明
我已经和一些刚接触 Java 的人谈过这个作用域概念。对一些人来说,这似乎是显而易见的,甚至是自然的。然而,对于其他人来说,这是一个持续困惑的原因。如果你属于后一种情况,不要担心,因为我们将在本章稍后再谈一些关于这个问题的内容,而在未来的章节中,我们将更深入地探讨作用域,并确保它不再是一个问题。
让我们看一个关于方法的另一个实际例子,并同时学到一些新东西。
探索方法重载
随着我们开始意识到,方法作为一个主题是相当深入的。但希望通过一步一步地学习,我们会发现它们并不令人畏惧。我们也将在下一章回到方法。现在,让我们创建一个新项目来探索方法重载的主题。
创建一个新的探索方法重载
,然后我们将继续编写三个方法,但稍微有些不同。
正如我们将很快看到的,我们可以创建多个具有相同名称的方法,只要参数不同。这个项目中的代码很简单。它的工作方式可能看起来有点奇怪,直到我们分析它之后。
在第一个方法中,我们将简单地称之为printStuff
,并通过参数传递一个int
变量进行打印。
将此方法插入在onCreate
方法的}
之后,但在MainActivity
类的}
之前。记得以通常的方式导入Log
类:
void printStuff(int myInt){
Log.i("info", "This is the int only version");
Log.i("info", "myInt = "+ myInt);
}
在第二个方法中,我们还将称之为printStuff
,但传入一个String
变量进行打印。将此方法插入在onCreate
方法的}
之后,但在MainActivity
类的}
之前:
void printStuff(String myString){
Log.i("info", "This is the String only version");
Log.i("info", "myString = "+ myString);
}
在这第三个方法中,我们将再次称之为printStuff
,但传入一个String
变量和一个int
值进行打印。将此方法插入在onCreate
的}
之后,但在MainActivity
类的}
之前:
void printStuff(int myInt, String myString){
Log.i("info", "This is the combined int and String
version");
Log.i("info", "myInt = "+ myInt);
Log.i("info", "myString = "+ myString);
}
最后,在onCreate
方法的}
之前插入这段代码,以调用方法并将一些值打印到 logcat 窗口:
// Declare and initialize a String and an int
int anInt = 10;
String aString = "I am a string";
// Now call the different versions of printStuff
// The name stays the same, only the parameters vary
printStuff(anInt);
printStuff(aString);
printStuff(anInt, aString);
现在我们可以在模拟器或真实设备上运行应用程序。这是输出:
Info: This is the int only version
Info: myInt = 10
Info: This is the String only version
Info: myString = I am a string
Info: This is the combined int and String version
Info: myInt = 10
Info: myString = I am a string
正如你所看到的,Java 将具有相同名称的三个方法视为不同的方法。正如我们刚刚展示的那样,这是有用的。这被称为方法重载。
方法重载和覆盖的混淆
重载是指当我们有多个具有相同名称但不同参数的方法时。
覆盖是指用相同的名称和参数列表替换一个方法。
我们对重载和覆盖已经了解足够,可以完成这本书;但如果你勇敢而且思绪飘忽,是的,你可以覆盖一个重载的方法,但这是另一个时间的事情。
这就是它的工作原理。在我们编写代码的每个步骤中,我们创建了一个名为printStuff
的方法。但是每个printStuff
方法都有不同的参数,因此实际上是可以单独调用的不同方法:
void printStuff(int myInt){
...
}
void printStuff(String myString){
...
}
void printStuff(int myInt, String myString){
...
}
每个方法的主体都是微不足道的,只是打印出传入的参数,并确认调用的方法版本。
我们代码的下一个重要部分是当我们明确指出要调用的方法版本,使用与签名中参数匹配的特定参数。在最后一步,我们依次调用每个方法,使用匹配的参数,这样 Java 就知道需要调用的确切方法:
printStuff(anInt);
printStuff(aString);
printStuff(anInt, aString);
现在我们已经知道关于方法的所有需要知道的东西,让我们快速再看一下方法和变量之间的关系。然后,我们会更深入地了解这个作用域现象。
重新访问作用域和变量
你可能还记得在真实世界方法
项目中,稍微令人不安的异常是,一个方法中的变量似乎与另一个方法中的变量不同,即使它们有相同的名称。如果你在一个方法中声明一个变量,无论是生命周期方法还是我们自己的方法,它只能在该方法内使用。
如果我们在onCreate
中这样做是没有用的:
int a = 0;
然后,在onPause
或其他方法中,我们尝试这样做:
a++;
我们会得到一个错误,因为a
只在它声明的方法中可见。起初,这可能看起来像是一个问题,但令人惊讶的是,这实际上是 Java 的一个特别有用的特性。
我已经提到用来描述这一点的术语是作用域。当变量可用时,就说它在作用域内,当不可用时,就说它不在作用域内。作用域的主题最好与类一起讨论,我们将在第十章**,面向对象编程和第十一章**,更多面向对象编程中这样做,但是作为对未来的一瞥,你可能想知道一个类可以有自己的变量,当它有时,它们对整个类都有作用域;也就是说,所有的方法都可以“看到”并使用它们。我们称它们为成员变量或字段。
要声明一个成员变量,只需在类的开始之后使用通常的语法,在类中声明的任何方法之外。假设我们的应用程序像这样开始:
public class MainActivity extends AppCompatActivity {
int mSomeVariable = 0;
// Rest of code and methods follow as usual
// ...
我们可以在这个类的任何方法中使用mSomeVariable
。我们的新变量mSomeVariable
只是为了提醒我们它是一个成员变量,所以在变量名中加上了m
。这不是编译器要求的,但这是一个有用的约定。
在我们继续讲解类之前,让我们再看一个方法的主题。
方法递归
方法递归是指一个方法调用自身。这乍一看可能像是一个错误,但实际上是解决一些编程问题的有效技术。
这里有一些代码展示了一个递归方法的最基本形式:
void recursiveMethod() {
recursiveMethod();
}
如果我们调用recursiveMethod
方法,它的唯一代码行将调用自身,然后再调用自身,然后再调用自身,依此类推。这个过程将永远持续下去,直到应用程序崩溃,在 Logcat 中会出现以下错误:
java.lang.StackOverflowError: stack size 8192KB
当方法被调用时,它的指令被移动到处理器的一个区域,称为堆栈,当它返回时,它的指令被移除。如果方法从不返回,而是不断添加更多的指令副本,最终堆栈将耗尽内存(或溢出),我们会得到StackOverflowError
。
我们可以尝试使用下一个截图来可视化前四个方法调用。此外,在下一个截图中,我划掉了对方法的调用,以显示如果我们能够在某个时刻阻止方法调用,最终所有的方法都将返回并从堆栈中清除:
图 9.2 - 方法调用
为了使我们的递归方法有价值,我们需要增强两个方面。我们将很快看到第二个方面。首先,最明显的是,我们需要给它一个目的。我们可以让我们的递归方法求和(相加)从 0 到给定目标值(比如 10、100 或更多)范围内的数字的值。让我们通过给它这个新目的并相应地重命名它来修改前面的方法。我们还将添加一个具有类范围(在方法之外)的变量answer
:
int answer = 0;
void computeSum(int target) {
answer += target;
computeSum(target-1);
}
现在我们有一个名为computeSum
的方法,它以一个int
作为参数。如果我们想要计算 0 到 10 之间所有数字的总和,我们可以这样调用该方法:
computeSum(10);
以下是每个函数调用时answer
变量的值:
第一次调用computeSum
:answer
= 10
第二次调用computeSum
:answer
= 19
…
第十次调用computeSum
:answer
= 55
表面上看成功 - 直到你意识到该方法在target
变量达到 0 之后仍然继续调用自身。事实上,我们仍然面临着第一个递归方法的相同问题,经过数万次方法调用后,应用程序将再次崩溃并出现StackOverflowError
。
我们需要一种方法来阻止方法在target
等于 0 时继续调用自身。我们解决这个问题的方法是检查target
的值是否为 0,如果是,我们就停止调用该方法。看看下面显示的额外突出显示的代码:
void computeSum(int target) {
answer += target;
if(target > 0) {
Log.d("target = ", "" + target);
computeSum(target - 1);
}
Log.d("answer = ", "" + answer);
我们使用if
语句来检查目标变量是否大于 0。当方法被最后一次调用时,我们还有额外的Log.d
代码来输出answer
的值。在阅读输出后的解释之前,看看你能否弄清楚发生了什么。
调用computeSum(10)
的输出如下:
target =: 10
target =: 9
target =: 8
target =: 7
target =: 6
target =: 5
target =: 4
target =: 3
target =: 2
target =: 1
answer =: 55
if(target > 0)
告诉代码首先检查target
变量是否大于 0。如果是,然后才调用方法并传入target - 1
的值。如果不是,那么它就停止整个过程。
重要提示
我们不会在本书中使用方法递归,但这是一个有趣的概念需要理解。
我们对方法了解得足够多,可以完成书中的所有项目。让我们通过一些问题和答案进行一个快速回顾。
问题
- 这个方法定义有什么问题?
doSomething(){
// Do something here
}
没有声明返回类型。你不必从方法中返回一个值,但在这种情况下它的返回类型必须是void
。方法应该如下所示:
void doSomething(){
// Do something here
}
- 这个方法定义有什么问题?
float getBalance(){
String customerName = "Linus Torvalds";
float balance = 429.66f;
return userName;
}
该方法返回一个字符串(userName
),但签名规定它必须返回一个float
类型。以getBalance
这样的方法名,这段代码可能是原本想要的:
float getBalance(){
String customerName = "Linus Torvalds";
float balance = 429.66f;
return balance;
}
- 我们什么时候调用
onCreate
方法?(提示:这是一个诡计问题!)
我们不需要。Android 决定何时调用onCreate
方法,以及构成 Activity 生命周期的所有其他方法。我们只覆盖对我们有用的方法。但是,我们会调用super.onCreate
,以便我们的重写版本和原始版本都被执行。
重要提示
为了完全披露,从我们的代码中技术上可以调用生命周期方法,但在本书的上下文中我们永远不需要这样做。最好将这些事情留给 Android。
总结
在前五章中,我们对各种小部件和其他 UI 元素变得相当熟练。我们还构建了广泛的 UI 布局选择。在本章和前三章中,我们已经相当深入地探索了 Java 和 Android 活动生命周期,尤其是考虑到我们完成得多么快。
我们在一定程度上创建了 Java 代码和 UI 之间的交互。我们通过设置onClick
属性调用了我们的方法,并使用setContentView
方法加载了我们的 UI 布局。然而,我们并没有真正建立 UI 和 Java 代码之间的适当连接。
我们现在真正需要做的是将这些东西结合起来,这样我们就可以开始使用 Android UI 来显示和操作我们的数据。为了实现这一点,我们需要更多地了解类的知识。
自从《第一章》《开始 Android 和 Java》以来,类一直潜伏在我们的代码中,我们甚至有点使用它们。然而,直到现在,除了不断地参考《第十章》《面向对象编程》之外,我们还没有适当地解决它们。在下一章《第十章》《面向对象编程》中,我们将快速掌握类的知识,然后我们终于可以开始构建应用程序,使 UI 设计和我们的 Java 代码完美地协同工作。
进一步阅读
我们已经学到了足够的 Java 知识来继续阅读本书。然而,看到更多 Java 实例并超越最低必要的知识总是有益的。如果你想要一个学习 Java 更深入的好资源,那么官方的 Oracle 网站是一个不错的选择。请注意,您不需要学习这个网站来继续阅读本书。另外,请注意,Oracle 网站上的教程并不是在 Android 环境中设置的。该网站是一个有用的资源,可以收藏并浏览:
-
官方 Java 教程:
docs.oracle.com/javase/tutorial/
-
官方 Android 开发者网站:
developer.android.com/training/basics/firstapp
第十章:面向对象编程
在本章中,我们将发现在 Java 中,类对几乎所有事情都是基础的。我们还将开始理解为什么 Sun Microsystems 的软件工程师在 20 世纪 90 年代初让 Java 成为现在这个样子。
我们已经谈论了重用其他人的代码,特别是 Android API,但在本章中,我们将真正掌握这是如何工作的,并学习面向对象编程以及如何使用它。
总之,我们将涵盖以下主题:
-
面向对象编程是什么,包括封装、继承和多态
-
在应用程序中编写和使用我们的第一个类
技术要求
你可以在 GitHub 上找到本章的代码文件,网址为github.com/PacktPublishing/Android-Programming-for-Beginners-Third-Edition/tree/main/chapter%2010
。
重要的内存管理警告
我是在提到我们大脑的记忆。如果你试图记住本章(或下一章),你将不得不在你的大脑中腾出很多空间,你可能会忘记一些非常重要的事情——比如去工作或感谢作者告诉你不要试图记住这些东西。
一个很好的目标将是尽量接近理解。这样,你的理解将变得更全面。然后在需要时,你可以参考本章(或下一章)进行复习。
提示
如果你对本章的内容不完全理解也没关系!继续阅读并确保完成所有的应用程序。
面向对象编程
在第一章**,开始 Android 和 Java中,我们提到 Java 是一种面向对象的语言。面向对象的语言要求我们使用面向对象 编程(OOP)。这不像汽车上的赛车扰流板或游戏 PC 上的跳动 LED 那样是可选的额外部分。它是 Java 的一部分,因此也是 Android 的一部分。
让我们多了解一点。
OOP 究竟是什么?
面向对象编程是一种将我们的需求分解为比整体更易管理的块的编程方式。
每个块都是自包含的,但也可能被其他程序重复使用,同时与其他块一起工作。
这些块是我们所说的对象。当我们计划/编写一个对象时,我们使用一个类。一个类可以被看作是一个对象的蓝图。
我们实现一个类的对象。这被称为类的一个实例。想象一下房屋蓝图。你不能住在里面,但你可以从中建造一座房子;你建造它的一个实例。通常当我们为我们的应用设计类时,我们写它们来代表现实世界的事物。
然而,面向对象编程不仅仅是这样。它也是一种做事情的方式——一种定义最佳实践的方法。
面向对象编程的三个核心原则是封装、多态和继承。这听起来可能很复杂,但一步一步来,是相当简单的。
封装
封装意味着保持代码的内部工作不受使用它的代码的干扰,只允许访问你选择的变量和方法。
这意味着你的代码总是可以更新、扩展或改进,而不会影响使用它的程序,只要暴露的部分仍然以相同的方式访问。
还记得第一章**,开始 Android 和 Java中的这行代码吗?
locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
通过适当的封装,卫星公司或 Android API 团队需要更新他们的代码工作方式都无关紧要。只要getLastKnownLocation
方法签名保持不变,我们就不必担心内部发生了什么。我们在更新之前编写的代码在更新后仍将正常工作。
如果汽车制造商去掉车轮,将其改为电动悬浮汽车,只要它仍然有方向盘、油门和刹车踏板,驾驶它应该不会太具有挑战性。
重要提示
当我们使用 Android API 的类时,我们是按照 Android 开发人员设计他们的类的方式来使用的。
多态性
多态性允许我们编写的代码对我们试图操作的类型不那么依赖,使我们的代码更清晰、更高效。多态性意味着不同的形式。如果我们编写的对象可以是多种类型的东西,那么我们就可以利用这一点。下一章中的一些例子将说明这一点,接下来的类比将让您从现实世界的角度看清楚。
如果我们有一个汽车工厂,只需改变给机器人的指令和放在生产线上的零件,就可以制造货车和小型卡车,那么这个工厂就是在使用多态性。
如果我们能够编写能够处理不同类型数据的代码而不必重新开始,那不是很有用吗?我们将在《第十一章》[更多面向对象编程]中看到一些例子。
继承
就像听起来的那样,extends
关键字:
public class MainActivity extends AppCompatActivity {
AppCompatActivity
类本身继承自Activity
。因此,每次我们创建一个新的 Android 项目时,我们都是从Activity
继承的。我们可以进一步了解它的有用之处。
想象一下,如果世界上最强壮的男人和最聪明的女人在一起。他们的孩子很可能会从基因继承中获得重大好处。在 Java 中,继承让我们可以用另一个人的代码和我们自己的代码做同样的事情,创建一个更符合我们需求的类。
为什么要这样做?
当正确编写时,所有这些面向对象编程允许您添加新功能,而不必过多担心它们如何与现有功能交互。当您确实需要更改一个类时,它的自包含(封装)性质意味着对程序的其他部分的影响会更少,甚至可能为零。这就是封装的部分。
您可以使用其他人的代码(例如 Android API),而不必知道或甚至关心它是如何工作的:想想一下 Android 生命周期、Toast
、Log
、所有 UI 小部件、监听卫星等等。例如,Button
类有近 50 个方法-我们真的想要为一个按钮自己编写所有这些吗?最好使用别人的Button
类。
面向对象编程允许您轻松编写高度复杂的应用程序。
通过继承,您可以创建多个类似但不同的类的版本,而无需从头开始编写类;并且您仍然可以使用原始类型对象的方法来处理新对象,这是由于多态性。
这真的很有道理。Java 从一开始就考虑到了所有这些,所以我们被迫使用所有这些面向对象编程;然而,这是一件好事。
让我们快速回顾一下类。
类回顾
类是一组代码行,可以包含方法、变量、循环和我们学过的所有其他 Java 语法。类是 Java 包的一部分,大多数包通常会有多个类。通常情况下,每个新类都将在其自己的.java
代码文件中定义,文件名与类名相同-就像我们迄今为止所有的 Activity 类一样。
一旦我们编写了一个类,我们可以使用它来制作任意数量的对象。记住,类是蓝图,我们根据蓝图制作对象。房子不是蓝图,就像对象不是类一样;它是从类制作的对象。对象是一个引用变量,就像String
变量一样,稍后我们将确切了解引用变量的含义。现在,让我们看一些实际的代码。
查看类的代码
假设我们正在为军方制作一个应用程序。这是供高级军官在战斗中微观管理他们的部队使用的。除其他外,我们可能需要一个代表士兵的类。
类的实现
这是我们假想类的真实代码。我们称之为一个类Soldier
,如果我们真的实现了这个,我们会在一个名为Soldier.java
的文件中这样做:
public class Soldier {
// Member variables
int health;
String soldierType;
// Method of the class
void shootEnemy(){
// Bang! Bang!
}
}
上述是一个名为Soldier
的类实现。有两个名为health
的 int 变量,以及一个名为soldierType
的 String 变量。
还有一个名为shootEnemy
的方法。该方法没有参数,返回类型为void
,但类方法可以是我们在*第九章**中讨论的任何形状或大小的方法。
要准确地了解成员变量和字段,当类被实例化为一个真实对象时,字段将成为对象本身的变量,我们称它们为实例或成员变量。
它们只是类的变量,无论它们被引用的名称有多么花哨。然而,字段和方法中声明的变量(称为局部变量)之间的区别随着我们的进展变得更加重要。
我们在第九章**结尾简要讨论了变量作用域,学习 Java 方法我们将在下一章再次看到所有类型的变量。让我们集中精力编写和使用一个类。
声明、初始化和使用类的对象
记住,Soldier
只是一个类,不是一个实际可用的对象。它是一个士兵的蓝图,而不是一个实际的士兵对象,就像int
、String
和boolean
不是变量一样;它们只是我们可以制作变量的类型。这是我们如何从我们的Soldier
类中制作一个类型为Soldier
的对象:
Soldier mySoldier = new Soldier();
在代码的第一部分中,Soldier mySoldier
声明了一个名为mySoldier
的类型为Soldier
的新变量。代码的最后一部分new Soldier()
调用了一个特殊的方法,称为构造方法,这个方法由编译器为所有类自动生成。
正是这个构造方法创建了一个实际的Soldier
对象。正如你所看到的,构造方法的名称与类的名称相同。我们将在本章后面更深入地研究构造函数。
当然,两部分中间的赋值运算符=
将第二部分的结果分配给第一部分的结果。下一张图总结了所有这些信息:
图 10.1 - 声明、初始化和使用类的对象
这与我们处理常规变量的方式并不相距太远,只是构造函数/方法调用而不是代码行末的值。要创建和使用一个非常基本的类,我们已经做得足够多了。
重要说明
正如我们将在进一步探讨时看到的,我们可以编写自己的构造函数,而不是依赖于自动生成的构造函数。这给了我们很多力量和灵活性,但现在我们将继续探讨最简单的情况。
就像普通变量一样,我们也可以像这样分两部分完成。
Soldier mySoldier;
mySoldier = new Soldier();
这是我们可能分配和使用假想类的变量的方式:
mySoldier.health = 100;
mySoldier.soldierType = "sniper";
// Notice that we use the object name mySoldier.
// Not the class name Soldier.
// We didn't do this:
// Soldier.health = 100;
// ERROR!
在这里,点运算符.
用于访问类的变量。这就是我们调用方法的方式 - 再次,通过使用对象名称,而不是类名称,后跟点运算符:
mySoldier.shootEnemy();
我们可以用图表总结点运算符的使用:
图 10.2 - 点运算符
提示
我们可以将类的方法视为它可以做的事情,将其实例/成员变量视为它了解自身的事情。
我们也可以继续制作另一个Soldier
对象并访问它的方法和变量:
Soldier mySoldier2 = new Soldier();
mySoldier2.health = 150;
mySoldier2.soldierType = "special forces";
mySoldier2.shootEnemy();
重要的是要意识到mySoldier2
是一个完全独立的对象,具有完全不同的实例变量,与mySoldier
不同:
图 10.3 - 士兵对象
这里还有一个关键点,即前面的代码不会在类本身内部编写。例如,我们可以在名为Soldier.java
的外部文件中创建Soldier
类,然后使用我们刚刚看到的代码,可能在MainActivity
类中。
当我们在一分钟内在实际项目中编写我们的第一个类时,这将变得更加清晰。
还要注意,所有操作都是在对象本身上进行的。我们必须创建类的对象才能使它们有用。
重要提示
像往常一样,这个规则也有例外。但它们是少数,我们将在下一章中看到这些例外。实际上,到目前为止,我们已经看到了两个例外。我们已经看到的例外是Toast
和Log
类。它们的具体情况将很快得到解释。
让我们通过编写一个真正的基本类来更深入地探索基本类。
基本类应用
将使用我们的应用程序的将军需要不止一个Soldier
对象。在我们即将构建的应用程序中,我们将实例化和使用多个对象。我们还将演示使用变量和方法上的点运算符,以表明不同的对象具有自己的实例变量。
您可以在代码下载中获取此示例的完整代码。它在第十章/Basic Classes
文件夹中。但是,继续阅读以创建您自己的工作示例会更有用。
使用Basic
Classes
创建一个项目。现在我们将创建一个名为Soldier
的新类:
-
右键单击项目资源管理器窗口中的
com.yourdomain.basicclasses
(或者您的包名称)文件夹。 -
选择New | Java Class。
-
在
Soldier
中按下Enter键。
为我们创建了一个新的类,其中包含一个代码模板,准备将我们的实现放入其中,就像下一个代码所示的那样。
package com.yourdomain.basicclasses;
public class Soldier {
}
注意,Android Studio 将类放在与我们应用程序的其他 Java 文件相同的包/文件夹中。
现在我们可以编写它的实现。
按照所示,在Soldier
类的开放和闭合大括号内编写以下类实现代码。要输入的新代码已经高亮显示:
public class Soldier {
int health;
String soldierType;
void shootEnemy(){
//let's print which type of soldier is shooting
Log.i(soldierType, " is shooting");
}
}
现在我们有了一个类,即将来的Soldier
类型对象的蓝图,我们可以开始建立我们的军队。在编辑窗口中,单击setContentView
方法调用后的onCreate
方法。输入以下代码:
// First, we make an object of type soldier
Soldier rambo = new Soldier();
rambo.soldierType = "Green Beret";
rambo.health = 150;
// It takes a lot to kill Rambo
// Now we make another Soldier object
Soldier vassily = new Soldier();
vassily.soldierType = "Sniper";
vassily.health = 50;
// Snipers have less health
// And one more Soldier object
Soldier wellington = new Soldier();
wellington.soldierType = "Sailor";
wellington.health = 100;
// He's tough but no green beret
现在我们有了极其多样化和不太可能的军队,我们可以使用它并验证每个对象的身份。
在上一步中的代码下面输入以下代码:
Log.i("Rambo's health = ", "" + rambo.health);
Log.i("Vassily's health = ", "" + vassily.health);
Log.i("Wellington's health = ", "" + wellington.health);
rambo.shootEnemy();
vassily.shootEnemy();
wellington.shootEnemy();
现在我们可以运行我们的应用程序。所有输出将显示在 logcat 窗口中。
这就是它的工作原理。首先,我们创建了我们的新Soldier
类。然后我们实现了我们的类,包括声明两个字段(成员变量),一个int
变量和一个名为health
和soldierType
的String
变量。
我们的类中还有一个名为shootEnemy
的方法。让我们再次看一下,并检查发生了什么:
void shootEnemy(){
//let's print which type of soldier is shooting
Log.i(soldierType, " is shooting");
}
在方法的主体中,我们打印到 logcat 窗口:首先是soldierType
,然后是文本" is shooting"
。这里很棒的是,字符串soldierType
会根据我们在shootEnemy
方法上调用的对象不同而不同。
接下来,我们声明并创建了三个Soldier
类型的新对象。它们是rambo
,vassily
和wellington
。
最后,我们为health
和soldierType
的每个值初始化了不同的值。
这是输出:
Rambo's health =: 150
Vassily's health =: 50
Wellington's health =: 100
Green Beret: is shooting
Sniper: is shooting
Sailor: is shooting
注意,每次访问每个Soldier
对象的health
变量时,它都会打印我们分配的值,证明尽管这三个对象是相同类型的,但它们是完全独立的个体实例/对象。
也许更有趣的是对shootEnemy
的三次调用。逐个地,我们的Soldier
对象的shootEnemy
方法被调用,并且我们将soldierType
变量打印到 logcat 窗口。该方法对每个单独的对象都有适当的值,进一步证明我们有三个不同的对象(类的实例),尽管它们是从同一个Soldier
类创建的。
我们看到每个对象都是完全独立的。然而,如果我们想象我们的应用中有整个军队的Soldier
对象,那么我们意识到我们需要学习处理大量对象(以及常规变量)的新方法。
想想管理 100 个独立的Soldier
对象。当我们有成千上万的对象时呢?此外,这并不是很动态。我们现在编写代码的方式依赖于我们(开发人员)知道将由将军(用户)指挥的士兵的确切细节。我们将在第十五章**,数组、映射和随机数中看到解决方案。
我们的第一个类还可以做更多的事情
我们可以像处理其他变量一样处理类。我们可以在方法签名中使用类作为参数,就像这样:
public void healSoldier(Soldier soldierToBeHealed){
// Use soldierToBeHealed here
// And because it is a reference the changes
// are reflected in the actual object passed into
// the method.
// Oops! I just mentioned what
// a reference variable can do
// More info in the FAQ, chapter 11, and onwards
}
当我们调用方法时,当然必须传递该类型的对象。以下是对healSoldier
方法的假设调用:
healSoldier(rambo);
当然,前面的例子可能会引发问题,比如,healSoldier
方法应该是一个类的方法吗?
fieldhospital.healSoldier(rambo);
可能是,也可能不是。这将取决于情况的最佳解决方案是什么。我们将看到更多的面向对象编程,然后对许多类似的难题的最佳解决方案应该更容易呈现出来。
而且,你可能会猜到,我们也可以将对象用作方法的返回值。以下是更新后的假设healSoldier
签名和实现可能看起来像的样子:
Soldier healSoldier(Soldier soldierToBeHealed){
soldierToBeHealed.health++;
return soldierToBeHealed;
}
实际上,我们已经看到类被用作参数。例如,这是我们来自第二章**,初次接触:Java、XML 和 UI 设计师的topClick
方法。它接收了一个名为v
的View
类型的对象:
public void topClick(View v){
然而,在topClick
方法的情况下,我们没有对传入的View
类型的对象做任何操作。部分原因是因为我们不需要,部分原因是因为我们不知道可以对View
类型的对象做什么 - 至少目前还不知道。
正如我在本章开头提到的,你不需要理解或记住本章的所有内容。掌握面向对象编程的唯一方法就是不断地使用它。就像学习口语一样 - 学习和研究语法规则会有所帮助,但远不及口头交流(或书面交流)来得有用。如果你差不多懂了,就继续下一章吧。
常见问题
- 我真的等不及了。引用到底是什么!?
它实际上就是在普通(非编程)语言中的引用。它是一个标识/指向数据的值,而不是实际的数据本身。一个思考它的方式是,引用是一个内存位置/地址。它标识并提供对内存中该位置/地址上的实际数据的访问。
- 如果它不是实际的对象,而只是一个引用,那么我们怎么能调用它的方法,比如
mySoldier.shootEnemy()
呢?
Java 在幕后处理了确切的细节,但你可以把引用看作是对象的控制器,你想对对象做的任何事情都必须通过控制器来做,因为实际的对象/内存本身不能直接访问。关于这一点,第十二章*,栈、堆和垃圾收集器*中有更多内容。
总结
我们终于编写了我们的第一个类。我们已经看到我们可以在与类同名的 Java 文件中实现一个类。类本身在我们实例化一个类的对象/实例之前并不起作用。一旦我们有了一个类的实例,我们就可以使用它的变量和方法。正如我们在基本类应用程序中证明的那样,每个类的实例都有自己独特的变量,就像当你买一辆工厂生产的汽车时,你会得到自己独特的方向盘、卫星导航和加速条纹。
所有这些信息都会引发更多的问题。面向对象编程就是这样。因此,让我们尝试通过再次查看变量和封装、多态性以及继承来巩固所有这些类的内容,下一章将展示它们的实际应用。然后我们可以进一步学习类,并探讨静态类(例如 Log 和 Toast)以及抽象类和接口等更高级的概念。
第十一章:更多面向对象编程
本章是我们对面向对象编程的风潮之旅(理论和实践)的第二部分。我们已经简要讨论了封装、继承和多态性的概念,但在本章中,我们将看到它们在一些演示应用程序中更加实际的运用。虽然工作示例将展示这些概念以其最简单的形式,但这仍然是朝着通过我们的 Java 代码控制布局的重要一步。
在本章中,我们将探讨以下内容:
-
深入了解封装及其帮助我们的方式
-
深入了解继承及如何充分利用
-
更详细地解释多态性
-
静态类及我们已经在使用的方式
-
抽象类和接口
首先,我们将处理封装。
技术要求
您可以在 GitHub 上找到本章中的代码文件github.com/PacktPublishing/Android-Programming-for-Beginners-Third-Edition/tree/main/chapter%2011
。
还记得封装吗?
到目前为止,我们真正看到的是一种代码组织约定,我们编写类,充满了变量和方法。我们确实讨论了所有这些面向对象编程的更广泛目标,但现在我们将进一步探讨,并开始看到我们如何实际通过面向对象编程实现封装。
封装的定义
封装描述了对象隐藏其数据和方法不让外界访问的能力,只允许访问您选择的变量和方法。这意味着您的代码始终可以更新、扩展或改进,而不会影响使用它的程序——只要暴露的部分仍然以相同的方式可访问。它还允许使用您封装的代码的代码变得更简单、更易于维护,因为任务的大部分复杂性都封装在您的代码中。
但是你不是说我们不需要知道内部发生了什么吗?所以你可能会像这样质疑我们迄今为止所看到的东西:如果我们不断地设置实例变量,比如rambo.health = 100;
,难道不可能最终出现问题,比如这样吗?
rambo.soldierType = "fluffy bunny";
封装保护了您的类的对象,使其无法以其不应有的方式使用。通过控制类代码的使用方式,它只能做您想要做的事情,并且具有您可以控制的值范围。
它不会被强制出现错误或崩溃。此外,您可以自由地更改代码的内部工作方式,而不会破坏程序的其余部分或使用旧版本代码的任何程序:
weightlifter.legstrength = 100;
weightlifter.armstrength = 1;
weightlifter.liftHeavyWeight();
// one typo and weightlifter rips own arms off
封装不仅对于编写其他人将使用的代码(例如我们使用的 Android API)至关重要,而且在编写自己将重复使用的代码时也是必不可少的,因为它将使我们免受自己的错误。此外,程序员团队将广泛使用封装,以便团队的不同成员可以在同一程序上工作,而不需要所有团队成员都知道其他团队成员的代码如何工作。我们可以为了获得同样的优势而封装我们的类,以下是如何做到的。
使用访问修饰符控制类的使用
类的设计者控制着任何使用其类的程序所能看到和操作的内容。我们可以像这样添加一个class
关键字:
public class Soldier{
//Implementation goes here
}
类访问修饰符
到目前为止,我们已经讨论了上下文中类的两个主要访问修饰符。让我们依次简要地看一下每一个:
-
public
:这很简单。声明为 public 的类可以被所有其他类看到。 -
default
:当未指定访问修饰符时,类具有默认访问权限。这将使其对同一包中的类公开,但对所有其他类不可访问。
现在我们可以开始封装这个东西了。但是,即使乍一看,所描述的访问修饰符也不是非常精细。我们似乎只能完全封锁包外的任何东西,或者完全自由。
实际上,这里的好处很容易被利用。想法是设计一个包含一组任务的类包。然后,包的所有复杂内部工作,那些不应该被任何人干扰的东西,应该是默认访问权限(只能被包内的类访问)。然后我们可以提供一些精心选择的公共类,供其他人(或程序的其他不同部分)使用。
重要说明
对于本书中应用程序的大小和复杂性来说,创建多个包是过度的。当然,我们将使用其他人的包和类(公共部分),所以了解这些内容是值得的。
类访问权限总结
一个设计良好的应用程序可能由一个或多个包组成,每个包只包含默认或默认和公共类。
除了类级别的隐私控制之外,Java 还为我们程序员提供了非常精细的控制,但要使用这些控制,我们必须更详细地研究变量。
使用访问修饰符控制变量的使用
为了加强类的可见性控制,我们有变量访问修饰符。这是一个声明了private
访问修饰符的变量:
private int myInt;
还要注意,我们对变量访问修饰符的所有讨论也适用于对象变量。例如,这里声明、创建和分配了我们的Soldier
类的一个实例。如你所见,这种情况下指定的访问权限是公共的:
public Soldier mySoldier = new Soldier();
在将修饰符应用于变量之前,必须首先考虑类的可见性。如果类a对类b不可见,比如因为类a具有默认访问权限,而类b在另一个包中,那么在类a的变量上使用任何访问修饰符都没有任何影响 - 类b无法看到其中任何一个。
因此,有必要在必要时向另一个类显示一个类,但只公开直接需要的变量 - 而不是所有的变量。
以下是不同变量访问修饰符的解释。
变量访问修饰符
变量访问修饰符比类访问修饰符更多,也更精细。访问修改的深度和复杂性不在于修饰符的范围,而在于我们可以如何巧妙地组合它们以实现封装的可贵目标。以下是变量访问修饰符:
-
public
:你猜对了,任何包中的任何类或方法都可以看到这个变量。只有当你确定这就是你想要的时候才使用public
。 -
protected
:这是继public
之后的下一个最不限制的。只要它们在同一个包中,protected
变量可以被任何类和任何方法看到。 -
default
:default
听起来不像protected
那么限制,但实际上更加限制。当没有指定访问权限时,变量具有default
访问权限。default
限制的事实或许意味着我们应该考虑隐藏我们的变量,而不是暴露它们。在这一点上,我们需要介绍一个新概念。你还记得我们曾简要讨论过继承以及如何可以快速地继承一个类的属性,然后使用extends
关键字对其进行改进吗?只是为了记录,default
访问权限的变量对子类是不可见的;也就是说,当我们像对 Activity 一样扩展一个类时,我们无法看到它的默认变量。我们将在本章后面更详细地讨论继承。 -
private
:private
变量只能在声明它们的类内部可见。这意味着,与默认访问权限一样,它们对子类(从所讨论的类继承的类)也是不可见的。
变量访问权限总结
一个设计良好的应用程序可能由一个或多个包组成,每个包只包含default
或default
和public
类。在这些类中,变量将具有精心选择和不同的访问修饰符,以实现我们封装目标的目标。
在我们开始实际操作之前,让我们再谈一下所有这些访问修改的东西中的一个小技巧。
方法也可以有访问修饰符
我们已经在第九章**,学习 Java 方法中简要提到,方法可以有访问修饰符。这是有道理的,因为方法是我们的类可以做的事情。我们将想要控制我们的类的用户可以做什么和不能做什么。
这里的一般想法是,一些方法将只在内部执行操作,因此不需要类的用户,而一些方法将是类的用户使用类的基础。
方法访问修饰符
方法的访问修饰符与类变量的访问修饰符相同。这使得事情容易记住,但再次表明,成功的封装是一种设计问题,而不是遵循任何特定规则的问题。
作为一个例子,只要它在一个公共类中,这种方法可以被任何其他类使用:
public useMeEverybody(){
//do something everyone needs to do here
}
而这个方法只能被它所属的类内部使用:
private secretInternalTask(){
/*
do something that helps the class function
internally
Perhaps, if it is part of the same class,
useMeEverybody could use this method...
On behalf of the classes outside of this class.
Neat!
*/
}
下一个没有指定访问权限的方法具有默认可见性。它只能被同一包中的其他类使用。如果我们扩展持有此“默认”访问方法的类,子类将无法访问此父类的方法:
fairlySecretTask(){
// allow just the classes in the package
// Not for external use
}
在我们继续之前的最后一个例子中,这是一个protected
方法,只对包可见,但可以被我们扩展它的类使用-就像onCreate
一样:
protected packageTask(){
// Allow just the classes in the package
// And you can use me if you extend me too
}
让我们快速回顾一下方法封装,但请记住,你不需要记住所有的东西。
方法访问总结
方法访问应该被选择为最好地执行我们已经讨论过的原则。它应该为你的类的用户提供他们所需要的访问权限,最好是没有更多。通过这样做,我们实现了我们的封装目标,比如保护代码的内部工作免受使用它的程序的干扰,出于我们已经讨论过的所有原因。
使用 getter 和 setter 访问私有变量
现在,如果将变量隐藏为私有是最佳实践,我们需要考虑如何允许访问它们,而不破坏我们的封装。如果Hospital
类的对象想要访问Soldier
类型的对象的health
成员变量,以便增加它,health
变量应该是私有的,因为我们不希望任何代码片段都可以更改它。
为了能够尽可能多地将成员变量设置为私有,同时仍然允许对其中一些进行有限访问,我们使用getter和setter。Getter 和 setter 只是获取和设置变量值的方法。
这不是我们必须学习的一些特殊的新的 Java 东西。这只是一个使用我们已经知道的东西的惯例。让我们看一下使用我们的Soldier
类和Hospital
类示例的 getter 和 setter。
在这个例子中,我们的两个类分别在自己的文件中创建,但在同一个包中。首先,这是我们假设的Hospital
类:
class Hospital{
private void healSoldier(Soldier soldierToHeal){
int health = soldierToHeal.getHealth();
health = health + 10;
soldierToHeal.setHealth(health);
}
}
我们的Hospital
类的实现只有一个方法,healSoldier
。它接收一个Soldier
对象的引用作为参数。因此,这个方法将在传入的任何Soldier
对象上工作:vassily
,wellington
,rambo
,或其他人。
它还有一个本地的health
变量,它用来临时保存并增加士兵的健康。在同一行中,它将health
变量初始化为Soldier
对象的当前健康状况。Soldier
对象的健康状况是私有的,因此使用公共的getHealth
getter 方法。
然后health
增加了 10,setHealth
setter 方法加载了新的恢复后的健康值,返回到Soldier
对象。
关键在于,尽管Hospital
对象可以改变Soldier
对象的健康状况,但它只能在 getter 和 setter 方法的范围内这样做。getter 和 setter 方法可以被编写来控制和检查可能错误的,甚至有害的值。
接下来,看看我们刚刚使用的假设的Soldier
类,它具有最简单的 getter 和 setter 方法的实现:
public class Soldier{
private int health;
public int getHealth(){
return health;
}
public void setHealth(int newHealth){
// Check for bad values of newHealth
health = newHealth;
}
}
我们有一个名为health
的实例变量,它是私有的。私有意味着它只能被Soldier
类的方法更改。然后我们有一个公共的getHealth
方法,它返回私有的health
int 变量中保存的值。由于这个方法是公共的,任何具有Soldier
类型对象访问权限的代码都可以使用它。
接下来,实现了setHealth
方法。同样,它是公共的,但这次它接受一个int
作为参数,并将传入的任何内容分配给私有的health
变量。在更像生活的例子中,我们会在这里编写更多的代码,以确保传入的值在我们期望的范围内。
现在我们声明、创建和赋值,创建每个新类的对象,并看看我们的 getter 和 setter 是如何工作的:
Soldier mySoldier = new Soldier();
// mySoldier.health = 100;//Doesn't work, private
// we can use the public setter setHealth() instead
mySoldier.setHealth(100); //That's better
Hospital militaryHospital = new Hospital();
// Oh no mySoldier has been wounded
mySoldier.setHealth(10);
/*
Take him to the hospital.
But my health variable is private
And Hospital won't be able to access it
I'm doomed - tell Laura I love her
No wait- what about my public getters and setters?
We can use the public getters and setters
from another class
*/
militaryHospital.healSoldier(mySoldier);
// mySoldiers private variable health has been increased
// by 10\. I'm feeling much better thanks!
我们看到我们可以直接在我们的Soldier
类型的对象上调用我们的公共setHealth
和getHealth
方法。不仅如此,我们还可以调用Hospital
对象的healSoldier
方法,传入对Soldier
对象的引用,后者也可以使用公共的 getter 和 setter 来操作私有的health
变量。
我们看到私有的health
变量是可以直接访问的,但完全受Soldier
类的设计者控制。
如果你想尝试一下这个示例,第十一章文件夹中的代码包中有一个名为GettersAndSetters
的工作应用程序的代码。我已经添加了几行代码来打印到控制台。
重要提示
Getter 和 setter 有时被称为它们更正确的名称访问器和修改器。我们将坚持使用 getter 和 setter。我只是想让你知道这个行话。
我们的示例和解释可能又引发了更多问题。这很好。
通过使用封装特性(如访问控制),就像签署了一个关于如何使用和访问类、它的方法和变量的重要协议。这份合同不仅仅是关于现在的协议,而且是对未来的暗示保证。当我们继续阅读本章时,我们会看到更多的方式来完善和加强这份合同。
提示
在需要的时候使用封装,或者当然,如果你的雇主要求你使用它的话。在一些小型学习项目中,如本书中的一些示例中,封装通常是多余的。当然,除非你学习的主题就是封装本身。
我们在学习这些 Java OOP 的东西时,假设你将来会想要编写更复杂的应用程序,无论是在 Android 上还是其他使用 OOP 的平台上。此外,我们将使用 Android API 中广泛使用它的类,并且这也将帮助我们理解那时发生了什么。通常情况下,在本书中,我们将在实现完整项目时使用封装,并经常忽略它,当展示单个想法或主题的小代码示例时。
使用构造函数设置我们的对象
有了这些私有变量及其 getter 和 setter,这是否意味着我们需要为每个私有变量都需要一个 getter 和 setter?那么对于一个有很多需要在开始时初始化的变量的类呢?想想以下情况:
mySoldier.name
mysoldier.type
mySoldier.weapon
mySoldier.regiment
...
其中一些变量可能需要 getter 和 setter,但如果我们只想在对象首次创建时设置一些东西,以使对象正确运行呢?
我们肯定不需要为每个变量都有两个方法(一个 getter 和一个 setter)吧?
幸运的是,这是不必要的。为了解决这个潜在的问题,有一个特殊的方法叫做构造函数。我们在第十章**面向对象编程中讨论实例化一个类的对象时,简要提到了构造函数的存在。让我们再看看构造函数。
在这里,我们创建了一个类型为Soldier
的对象,并将其赋给一个名为mySoldier
的对象:
Soldier mySoldier = new Soldier();
这里没有什么新的,但是看一下代码行的最后部分:
...Soldier();
这看起来可疑地像一个方法。
一直以来,我们一直在调用一个特殊的方法,称为构造函数,这个方法是由编译器在幕后自动创建的。
然而,现在到了重点,就像一个方法一样,我们可以覆盖构造函数,这意味着我们可以在使用新对象之前对其进行有用的设置。下面的代码展示了我们如何做到这一点:
public Soldier(){
// Someone is creating a new Soldier object
health = 200;
// more setup here
}
构造函数在语法上与方法有很多相似之处。但是,它只能在使用new
关键字的情况下调用,并且它是由编译器自动为我们创建的 - 除非我们像在先前的代码中那样创建自己的构造函数。
构造函数具有以下特点:
-
它们没有返回类型
-
它们与类具有完全相同的名称
-
它们可以有参数
-
它们可以被重载
在这一点上,还有一些 Java 语法是有用的,那就是 Java 的this
关键字。
当我们想要明确指出我们正在引用哪些变量时,就会使用this
关键字。再看看这个例子构造函数,再看一个假设的Soldier
类的变体:
public class Soldier{
String name;
String type;
int health;
// This is the constructor
// It is called when a new instance is created
public Soldier(String name, String type, int health){
// Someone is creating a new Soldier object
this.name = name;
this.type = type;
this.health = health;
// more setup here
}
}
这次,构造函数为每个我们想要初始化的变量都有一个参数。通过使用this
关键字,当我们指的是成员变量或参数时就很清楚。
关于变量和this
还有更多的技巧和转折,当应用到一个实际项目时,它们会更有意义。在下一个应用程序中,我们将探索本章迄今为止学到的所有内容,还有一些新的想法。
首先,再多一点面向对象编程。
静态方法
我们已经对类有相当多的了解。例如,我们知道如何将它们转换为对象并使用它们的方法和变量。但是有些地方不太对。自从书的开头,我们一直在使用两个类,比其他类更频繁地使用Log
和Toast
来输出到 logcat 或用户的屏幕,但我们从未实例化过它们!这怎么可能呢?我们从未这样做过:
Log myLog = new Log();
Toast myToast = new Toast();
我们直接使用了这些类,就像这样:
Log.i("info","our message here");
Toast.makeText(this, "our message",
Toast.LENGTH_SHORT).show();
类的静态方法可以在没有首先实例化类的对象的情况下使用。我们可以将其视为属于类的静态方法,而所有其他方法都属于类的对象/实例。
而且你现在可能已经意识到,Log
和Toast
都包含静态方法。要清楚:Log
和Toast
包含静态方法;它们本身仍然是类。
类既可以有静态方法,也可以有常规方法,但是常规方法需要以常规方式使用,通过类的实例/对象。
再看一下Log.i
的使用:
Log.i("info","our message here");
在这里,i
是静态访问的方法,该方法接受两个参数,都是 String 类型。
接下来,我们看到了Toast
类的静态方法makeText
的使用:
Toast.makeText(this, "our message",
Toast.LENGTH_SHORT).show();
Toast
类的makeText
方法接受三个参数。
第一个是this
,它是对当前类的引用。我们在谈论构造函数时看到,为了明确地引用对象的当前实例的成员变量,我们可以使用this.health
,this.regiment
等等。
当我们像在上一行代码中那样使用this
时,我们指的是类本身的实例;不是Toast
类,而是上一行代码中的this
是对方法所在的类的引用。在我们的例子中,我们已经从MainActivity
类中使用了它。
在 Android 中,许多事情都需要引用Activity
的实例才能完成它们的工作。在本书中,我们将经常传递this
(对Activity
的引用)来使 Android API 中的类/对象能够完成它们的工作。我们还将编写需要this
作为一个或多个方法参数的类。因此,我们将看到如何处理传递进来的this
。
makeText
方法的第二个参数当然是一个String
。
第三个参数是访问一个final
变量LENGTH_SHORT
,同样是通过类名而不是类的实例。如果我们像下一行代码那样声明一个变量:
public static final int LENGTH_SHORT = 1;
如果变量是在一个名为MyClass
的类中声明的,我们可以像这样访问变量:MyClass.LENGTH_SHORT
,并像任何其他变量一样使用它,但final
关键字确保变量的值永远不会改变。这种类型的变量被称为常量。
static
关键字对变量也有另一个影响,特别是当它不是一个常量(可以改变)时,我们将在我们的下一个应用程序中看到它的作用。
现在,如果你仔细看代码行的最后,显示一个Toast
消息给用户,你会看到另一个新的东西,.show()
。
这被称为Toast
类,但只使用了一行代码。实际触发消息的是show
方法。
当我们继续阅读本书时,我们将更仔细地研究链式调用,比如在第十四章中,Android 对话框窗口,当我们创建一个弹出对话框时。
提示
如果你想详细了解Toast
类及其其他一些方法,你可以在这里查看:developer.android.com/reference/android/widget/Toast.html
。
静态方法通常在具有如此通用用途的类中提供,以至于创建该类的对象是没有意义的。另一个非常有用的具有静态方法的类是Math
。这个类实际上是 Java API 的一部分,而不是 Android API 的一部分。
提示
想写一个计算器应用程序吗?使用Math
类的静态方法比你想象的要容易。你可以在这里查看它们:docs.oracle.com/javase/7/docs/api/java/lang/Math.html
。
如果你尝试这个,你需要以同样的方式导入Math
类,就像你导入我们使用过的所有其他类一样。接下来,我们可以尝试一个实际的迷你应用程序来理解封装和静态方法。
封装和静态方法迷你应用程序
我们已经看到了对变量和它们的作用域的访问是如何受控制的,我们最好看一个例子来了解它们的作用。这些不会是变量使用的实际实例,更多的是一个演示,以帮助理解类、方法和变量的访问修饰符,以及引用或原始和局部或实例变量的不同类型,以及静态和最终变量和this
关键字的新概念。
完成的代码在下载包的第十一章文件夹中。它被称为Access Scope This And Static
。
创建一个新的空活动项目,并将其命名为Access Scope This And Static
。
通过右键单击项目资源管理器中的现有MainActivity
类并单击AlienShip
来创建一个新类。
现在,我们将声明我们的新类和一些成员变量。请注意,numShips
是private
和static
。我们很快将看到这个变量在类的所有实例中是相同的。shieldStrength
变量是private
,shipName
是公共的:
public class AlienShip {
private static int numShips;
private int shieldStrength;
public String shipName;
接下来是构造函数。我们可以看到构造函数是公共的,没有返回类型,并且与类名相同-根据规则。在其中,我们递增了私有静态的numShips
变量。请记住,每当我们创建一个新的AlienShip
类型的对象时,这将发生。此外,构造函数使用私有的setShieldStrength
方法为私有变量shieldStrength
设置一个值:
public AlienShip(){
numShips++;
/*
Can call private methods from here because I am
part of the class.
If didn't have "this" then this call
might be less clear
But this "this" isn't strictly necessary
Because of "this" I am sure I am setting
the correct shieldStrength
*/
this.setShieldStrength(100);
}
这是公共静态的 getter 方法,这样AlienShip
外部的类就可以找出有多少个AlienShip
对象了。我们还将看到我们如何使用静态方法:
public static int getNumShips(){
return numShips;
}
这是我们的私有setShieldStrength
方法。我们本可以直接从类内部设置shieldStrength
,但下面的代码显示了我们如何通过使用this
关键字区分shieldStrength
局部变量/参数和shieldStrength
成员变量:
private void setShieldStrength(int shieldStrength){
// "this" distinguishes between the
// member variable shieldStrength
// And the local variable/parameter of the same name
this.shieldStrength = shieldStrength;
}
接下来的方法是 getter,这样其他类就可以读取但不能更改每个AlienShip
对象的护盾强度:
public int getShieldStrength(){
return this.shieldStrength;
}
现在我们有一个公共方法,每次击中AlienShip
对象时都可以调用。它只是打印到控制台,然后检测该对象的shieldStrength
是否为零。如果是,它调用destroyShip
方法,我们将在下面看到:
public void hitDetected(){
shieldStrength -=25;
Log.i("Incomiming: ","Bam!!");
if (shieldStrength == 0){
destroyShip();
}
}
最后,对于我们的AlienShip
类,我们将编写destroyShip
方法。我们打印一条消息,指示基于其shipName
已被销毁的飞船,并递减numShips
静态变量,以便我们可以跟踪类型AlienShip
的对象数量:
private void destroyShip(){
numShips--;
Log.i("Explosion: ", ""+this.shipName + "
destroyed");
}
} // End of the class
现在我们切换到我们的MainActivity
类,并编写一些使用我们新的AlienShip
类的代码。所有代码都放在setContentView
调用之后的onCreate
方法中。首先,我们创建两个名为girlShip
和boyShip
的新AlienShip
对象:
// every time we do this the constructor runs
AlienShip girlShip = new AlienShip();
AlienShip boyShip = new AlienShip();
在下一个代码中,看看我们如何获取numShips
的值。我们使用getNumShips
方法,就像我们所期望的那样。但是,仔细看语法。我们使用的是类名,而不是对象。我们还可以使用不是静态的方法访问静态变量。我们这样做是为了看到静态方法的运行方式:
// Look no objects but using the static method
Log.i("numShips: ", "" + AlienShip.getNumShips());
现在,我们为我们的公共shipName
字符串变量分配名称:
// This works because shipName is public
girlShip.shipName = "Corrine Yu";
boyShip.shipName = "Andre LaMothe";
在接下来的代码中,我们尝试直接为私有变量分配一个值。这是行不通的。然后我们使用公共的 getter 方法getShieldStrength
来打印出在构造函数中分配的shieldStrength
:
// This won't work because shieldStrength is private
// girlship.shieldStrength = 999;
// But we have a public getter
Log.i("girl shields: ", "" + girlShip.getShieldStrength());
Log.i("boy shields: ", "" + boyShip.getShieldStrength());
// And we can't do this because it's private
// boyship.setShieldStrength(1000000);
最后,我们通过使用hitDetected
方法来炸毁一些东西,并偶尔检查我们两个对象的shieldStrength
:
// let's shoot some ships
girlShip.hitDetected();
Log.i("girl shields: ", "" + girlShip.getShieldStrength());
Log.i("boy shields: ", "" + boyShip.getShieldStrength());
boyShip.hitDetected();
boyShip.hitDetected();
boyShip.hitDetected();
Log.i("girl shields: ", "" + girlShip.getShieldStrength());
Log.i("boy shields: ", "" + boyShip.getShieldStrength());
boyShip.hitDetected();//Ahhh!
Log.i("girl shields: ", "" + girlShip.getShieldStrength());
Log.i("boy shields: ", "" + boyShip.getShieldStrength());
当我们认为我们已经摧毁了一艘飞船时,我们再次使用我们的静态getNumShips
方法来查看我们的静态变量numShips
是否被destroyShip
方法改变:
Log.i("numShips: ", "" + AlienShip.getNumShips());
运行演示并查看控制台输出:
numShips: 2
girl shields: 100
boy shields: 100
Incomiming: Bam!!
girl shields:﹕ 75
boy shields:﹕ 100
Incomiming: Bam!!
Incomiming: Bam!!
girl shields:﹕ 75
boy shields:﹕ 25
Incomiming: Bam!!
Explosion: Andre LaMothe destroyed
girl shields: 75
boy shields: 0
numShips: 1
boy shields: 0
numShips: 1
在前面的示例中,我们看到我们可以通过使用this
关键字区分相同名称的局部变量和成员变量。我们还可以使用this
关键字编写代码,引用当前正在操作的对象。
我们看到静态变量-在这种情况下,numShips
-在所有实例中是一致的;此外,通过在构造函数中递增它,并在我们的destroyShip
方法中递减它,我们可以跟踪我们当前拥有的AlienShip
对象的数量。
我们还看到,我们可以使用静态方法,使用类名和点运算符而不是实际对象。
提示
是的,我知道这就像生活在房子的蓝图中一样-但这也非常有用。
最后,我们证明了如何使用访问修饰符隐藏和公开某些方法和变量。
接下来,我们将看一下继承的主题。
面向对象编程和继承
我们已经看到,我们可以通过实例化/创建来自 Android 等 API 的类的对象来使用其他人的代码。但是这整个 OOP 的东西甚至比那更深入。
如果有一个类有很多有用的功能,但不完全符合我们的要求,我们可以从该类继承,然后进一步完善或添加其工作方式和功能。
你可能会惊讶地听到我们已经这样做了。实际上,我们已经为我们创建的每个应用程序都这样做了。当我们使用extends
关键字时,我们正在继承。记住这一点:
public class MainActivity extends AppCompatActivity ...
在这里,我们继承了AppCompatActivity
类以及它的所有功能-更具体地说,类设计者希望我们能够访问的所有功能。以下是我们可以对我们扩展的类做的一些事情。
我们甚至可以重写一个方法并且仍然部分依赖于我们继承的类中的重写方法。例如,我们每次扩展AppCompatActivity
类时都重写了onCreate
方法。但是当我们这样做时,我们也调用了类设计者提供的默认实现:
super.onCreate(...
在第六章,Android 生命周期中,我们重写了几乎所有 Activity 类的生命周期方法。
我们主要讨论继承,以便我们了解周围发生的事情,并作为最终能够设计有用的类的第一步,我们或其他人可以扩展。
考虑到这一点,让我们看一些示例类,并看看我们如何扩展它们,只是为了看看语法并作为第一步,也为了能够说我们已经这样做了。
当我们看这一章的最后一个主要主题,多态性时,我们也将同时深入研究继承。这里有一些使用继承的代码。
这段代码将放在一个名为Animal.java
的文件中:
public class Animal{
// Some member variables
public int age;
public int weight;
public String type;
public int hungerLevel;
public void eat(){
hungerLevel--;
}
public void walk(){
hungerLevel++;
}
}
然后在一个名为Elephant.java
的单独文件中,我们可以这样做:
public class Elephant extends Animal{
public Elephant(int age, int weight){
this.age = age;
this.weight = weight;
this.type = "Elephant";
int hungerLevel = 0;
}
}
我们可以看到在前面的代码中,我们实现了一个名为Animal
的类,它有四个成员变量:age
,weight
,type
和hungerLevel
。它还有两个方法,eat
和walk
。
然后我们用Elephant
扩展了Animal
。Elephant
现在可以做任何Animal
可以做的事情,它也有所有的变量。
我们在Elephant
构造函数中初始化了Animal
的变量,Elephant
在创建对象时将两个变量(age
和weight
)传递给构造函数,并且为所有Elephant
对象分配了两个变量(type
和hungerLevel
)。
我们可以继续编写一堆其他扩展Animal
的类,也许是Lion
,Tiger
和ThreeToedSloth
。每个类都会有age
,weight
,type
和hungerLevel
,并且每个类都能walk
和eat
。
好像 OOP 已经不够有用了,我们现在可以模拟现实世界的对象。我们还看到,通过子类化/扩展/继承其他类,我们可以使 OOP 变得更加有用。我们可能想要学习的术语是被扩展的类是超类,继承超类的类是子类。我们也可以说父类和子类。
提示
像往常一样,我们可能会问关于继承的这个问题。为什么?原因是这样的:我们可以在父类中编写一次通用代码;我们可以更新该通用代码,所有继承自它的类也会更新。此外,子类只能使用公共/受保护的实例变量和方法。因此,如果设计得当,这也进一步增强了封装的目标。
让我们构建另一个小应用程序来玩一下继承。
继承示例应用程序
我们已经看过了如何创建类的层次结构来模拟适合我们应用程序的系统。因此,让我们尝试一些使用继承的简单代码。完成的代码在Chapter 11文件夹中。它被称为Inheritance Example
。
使用AlienShip
、另一个Fighter
和最后一个Bomber
创建一个名为Inheritance Example
的新项目。
以下是AlienShip
类的代码。它与我们之前的类演示AlienShip
非常相似。不同之处在于构造函数现在接受一个int
参数,它用于设置护盾强度。
构造函数还会将消息输出到 logcat 窗口,这样我们就可以看到它何时被使用。AlienShip
类还有一个新方法fireWeapon
,声明为abstract
。
将一个类声明为抽象类可以保证任何子类AlienShip
都必须实现其自己的fireWeapon
版本。注意类的声明中有abstract
关键字。我们必须这样做是因为它的一个方法也使用了abstract
关键字。当我们讨论这个演示和下一节中讨论多态时,我们将解释abstract
方法和abstract
类。
将以下代码添加到AlienShip
类中:
public abstract class AlienShip {
private static int numShips;
private int shieldStrength;
public String shipName;
public AlienShip(int shieldStrength){
Log.i("Location: ", "AlienShip constructor");
numShips++;
setShieldStrength(shieldStrength);
}
public abstract void fireWeapon();
// Ahh my body where is it?
public static int getNumShips(){
return numShips;
}
private void setShieldStrength(int shieldStrength){
this.shieldStrength = shieldStrength;
}
public int getShieldStrength(){
return this.shieldStrength;
}
public void hitDetected(){
shieldStrength -=25;
Log.i("Incomiming: ", "Bam!!");
if (shieldStrength == 0){
destroyShip();
}
}
private void destroyShip(){
numShips--;
Log.i("Explosion: ", "" + this.shipName + "
destroyed");
}
}
现在我们将实现Bomber
类。注意调用super(100)
。这将使用shieldStrength
的值调用超类的构造函数。我们可以在这个构造函数中进一步初始化特定的Bomber
,但现在,我们只是打印出位置,这样我们就可以看到Bomber
构造函数何时被执行。因为我们必须,我们还必须实现一个抽象fireWeapon
方法的Bomber
特定版本。将以下代码添加到Bomber
类中:
public class Bomber extends AlienShip {
public Bomber(){
super(100);
// Weak shields for a bomber
Log.i("Location: ", "Bomber constructor");
}
public void fireWeapon(){
Log.i("Firing weapon: ", "bombs away");
}
}
现在我们将实现Fighter
类。注意调用super(400)
。这将使用shieldStrength
的值调用超类的构造函数。我们可以在这个构造函数中进一步初始化特定的Fighter
,但现在,我们只是打印出位置,这样我们就可以看到Fighter
构造函数何时被执行。我们还必须实现一个抽象fireWeapon
方法的Fighter
特定版本。将以下代码添加到Fighter
类中:
public class Fighter extends AlienShip{
public Fighter(){
super(400);
// Strong shields for a fighter
Log.i("Location: ", "Fighter constructor");
}
public void fireWeapon(){
Log.i("Firing weapon: ", "lasers firing");
}
}
接下来,我们将编写MainActivity
的onCreate
方法。像往常一样,在调用setContentView
方法之后输入此代码。这是使用我们的三个新类的代码。代码看起来相当普通 - 没有什么新东西。有趣的是输出:
Fighter aFighter = new Fighter();
Bomber aBomber = new Bomber();
// Can't do this AlienShip is abstract -
// Literally speaking as well as in code
// AlienShip alienShip = new AlienShip(500);
// But our objects of the subclasses can still do
// everything the AlienShip is meant to do
aBomber.shipName = "Newell Bomber";
aFighter.shipName = "Meier Fighter";
// And because of the overridden constructor
// That still calls the super constructor
// They have unique properties
Log.i("aFighter Shield:", ""+ aFighter.getShieldStrength());
Log.i("aBomber Shield:", ""+ aBomber.getShieldStrength());
// As well as certain things in certain ways
// That are unique to the subclass
aBomber.fireWeapon();
aFighter.fireWeapon();
// Take down those alien ships
// Focus on the bomber it has a weaker shield
aBomber.hitDetected();
aBomber.hitDetected();
aBomber.hitDetected();
aBomber.hitDetected();
运行应用程序,您将在 logcat 窗口中获得以下输出:
Location:﹕ AlienShip constructor
Location:﹕ Fighter constructor
Location:﹕ AlienShip constructor
Location:﹕ Bomber constructor
aFighter Shield:﹕ 400
aBomber Shield:﹕ 100
Firing weapon:﹕ bombs away
Firing weapon:﹕ lasers firing
Incomiming:﹕ Bam!!
Incomiming:﹕ Bam!!
Incomiming:﹕ Bam!!
Incomiming:﹕ Bam!!
Explosion:﹕ Newell Bomber destroyed
我们可以看到子类的构造函数如何调用超类的构造函数。我们还可以清楚地看到fireWeapon
方法的各个实现正如预期地工作。
让我们更仔细地看一下最后一个重要的面向对象编程概念,多态。然后我们将能够在 Android API 中做一些更实际的事情。
多态
我们已经知道多态意味着不同的形式。但对我们来说意味着什么呢?
简而言之:
任何子类都可以作为使用超类的代码的一部分。
这意味着我们可以编写更简单、更容易理解和更容易更改的代码。
此外,我们可以为超类编写代码,并依赖于这样一个事实:无论它被子类化多少次,在一定的参数范围内,代码仍然可以正常工作。让我们讨论一个例子。
假设我们想要使用多态来帮助编写一个动物园管理应用程序。我们可能希望有一个feed
之类的方法。我们可能希望将要喂养的动物的引用传递到feed
方法中。这似乎需要为每种类型的Animal
编写一个feed
方法。
然而,我们可以编写具有多态返回类型和参数的多态方法:
Animal feed(Animal animalToFeed){
// Feed any animal here
return animalToFeed;
}
前面的方法将Animal
作为参数,这意味着可以将从扩展Animal
的类构建的任何对象传递给它。正如你在前面的代码中看到的,该方法也返回Animal
,具有完全相同的好处。
多态返回类型有一个小问题,那就是我们需要意识到返回的是什么,并且在调用方法的代码中明确表示出来。
例如,我们可以像这样处理将Elephant
传递到feed
方法中:
someElephant = (Elephant) feed(someElephant);
注意前面代码中的(Elephant)
。这明确表示我们想要从返回的Animal
中得到Elephant
。这被称为转换。我们将在本书的其余部分偶尔使用转换。
因此,你甚至可以今天编写代码,并在一周、一个月或一年后创建另一个子类,而相同的方法和数据结构仍然可以工作。
此外,我们可以对我们的子类强制执行一组规则,规定它们可以做什么,不能做什么,以及如何做。因此,一个阶段的巧妙设计可以影响其他阶段。
但我们真的会想要实例化一个实际的Animal
吗?
抽象类
在继承示例应用程序中,我们使用了一个抽象类,但让我们深入一点。抽象类是一个不能被实例化的类;它不能被制作成一个对象。那么,它就是一个永远不会被使用的蓝图?但这就像付钱给建筑师设计你的房子,然后永远不建造它?你可能会对自己说,“我有点明白了抽象方法的概念,但抽象类就是愚蠢。”
如果我们或类的设计者想要强制我们在使用他们的类之前继承,他们可以将一个类声明为abstract。然后,我们就不能从中创建一个对象;因此,我们必须首先扩展它并从子类创建一个对象。
我们还可以声明一个方法为abstract
,然后该方法必须在扩展具有抽象方法的类的任何类中被重写。
让我们看一个例子——这会有所帮助。我们通过像这样使用abstract
关键字来声明一个类为abstract
:
abstract class someClass{
/*
All methods and variables here.
As usual!
Just don't try and make
an object out of me!
*/
}
是的,但为什么?
有时我们想要一个可以用作多态类型的类,但我们需要保证它永远不能被用作对象。例如,“动物”本身并没有太多意义。
我们不谈论动物;我们谈论动物的类型。我们不会说,“哦,看那只可爱的毛茸茸的白色动物。”或者,“昨天我们去宠物店买了一只动物和一个动物床。”这太,嗯,抽象了。
因此,抽象类有点像一个模板,可以被任何extends
它(继承自它)的类使用。
我们可能想要一个Worker
类,并将其扩展为Miner
、Steelworker
、OfficeWorker
,当然还有Programmer
。但一个普通的Worker
到底是做什么的呢?我们为什么要实例化一个?
答案是我们可能不想实例化一个;但我们可能想要将其用作多态类型,这样我们可以在方法之间传递多个工作子类,并且可以拥有可以容纳所有类型的Worker
的数据结构。
我们称这种类型的类为抽象类,当一个类有一个抽象方法时,它必须被声明为抽象类。并且所有抽象方法必须被任何扩展它的类重写。
这意味着抽象类可以提供一些在其所有子类中都可用的共同功能。例如,Worker
类可能有height
、weight
和age
成员变量。
它可能还有getPayCheck
方法,这不是抽象的,并且在所有子类中都是相同的,但doWork
方法是抽象的,必须被重写,因为所有不同类型的工作者都会以不同的方式doWork
。
这使我们顺利地进入了多态的另一个领域,这将在本书中为我们带来更多便利。
接口
接口就像一个类。哦!这里没有什么复杂的。但它就像一个始终是抽象的类,只有抽象方法。
我们可以将接口视为一个完全抽象的类,其中所有方法都是抽象的,也没有成员变量。
好吧,你大概可以理解抽象类,因为至少它可以在其方法中传递一些功能,这些功能不是抽象的,并且可以作为多态类型。但说真的,这个接口似乎有点毫无意义。
让我们看一个最简单的通用接口示例,然后我们可以进一步讨论它。
要定义一个接口,我们输入以下内容:
public interface someInterface{
void someAbstractMethod();
// omg I've got no body
int anotherAbstractMethod();
// Ahh! Me too
// Interface methods are always abstract and public
// implicitly but we could make it explicit if we prefer
public abstract void
explicitlyAbstractAndPublicMethod();
// still no body though
}
接口的方法没有方法体,因为它们是抽象的,但它们仍然可以有返回类型和参数,或者没有。
要使用接口,我们在类声明后使用implements
关键字:
public class someClass implements someInterface{
// class stuff here
/*
Better implement the methods of the interface
or we will have errors.
And nothing will work
*/
public void someAbstractMethod(){
// code here if you like
// but just an empty implementation will do
}
public int anotherAbstractMethod(){
// code here if you like
// but just an empty implementation will do
// Must have a return type though
// as that is part of the contract
return 1;
}
Public void explicitlyAbstractAndPublicMethod(){
}
}
这使我们能够使用多态性来处理来自完全不相关的继承层次结构的多个不同对象。如果一个类实现了一个接口,整个东西就可以被传递或用作它本身 - 因为它就是那个东西。它是多态的(多种形式)。
我们甚至可以让一个类同时实现多个不同的接口。只需在每个接口之间加上逗号,并在implements
关键字后列出它们。只需确保实现所有必要的方法。
在本书中,我们将更频繁地使用 Android API 的接口,而不是编写我们自己的接口。在第十三章**,匿名类 - 使 Android 小部件活跃中,我们将在 Java Meet UI 应用程序中使用OnClickListener
接口。
许多事情在被点击时可能想要知道。也许是Button
或TextView
小部件等等。因此,使用接口,我们不需要为每种我们想要点击的 UI 元素类型编写不同的方法。
经常问的问题
- 这个类声明有什么问题?
private class someClass{
// class implementation goes here
}
没有私有类。类可以是公共的或默认的。公共类是公共的;默认类在其自己的包内是私有的。
- 封装是什么?
封装是我们如何以一种方式包含我们的变量、代码和方法,以仅暴露我们想要暴露给其他代码的部分和功能。
总结
在本章中,我们涵盖了比其他任何章节都更多的理论。如果你没有记住所有内容,或者有些代码看起来有点太深入了,那么你仍然完全成功了。
如果你只是理解 OOP 是通过封装、继承和多态编写可重用、可扩展和高效的代码,那么你就有成为 Java 大师的潜力。
简而言之,面向对象编程使我们能够在其他人不知道我们在编写代码时会做什么的情况下使用其他人的代码。
你所需要做的就是不断练习,因为我们将在整本书中不断地使用这些概念,所以你在这一点上甚至不需要已经掌握它们。
在下一章中,我们将重新讨论本章的一些概念,以及看一些 OOP 的新方面,以及它如何使我们的 Java 代码与我们的 XML 布局进行交互。
但首先,有一个重要的即将到来的新闻快讯!
重要提示
所有的 UI 元素 - TextView
、ConstraintLayout
、CalenderView
和Button
- 也是类。它们的属性是成员变量,它们有大量的方法,我们可以使用这些方法来做各种各样的事情。这可能会很有用。
在接下来的两章中,我们将更多地了解这一点,但首先,我们将看看 Android 如何处理垃圾。