26 KiB
1. 什么是面向对象?
对象(Object)这个概念广泛存在于我们的生活当中,一般只一个独立的个体,具备一定的特性(properties),并有一定的动作能力(capability)。例如,一个具体的人,具备属性(性别、年龄、升高...);同时具备一定的能力(说话、跑步、工作...)
面向对象编程是指把相关数据和函数按照对象的方式进行组织和分类,以便达到便于设计、实现、维护和增加代码重用率的目的。面向对象编程中的类(Class)与对象(Object)与我们常用语境中的概念有所不同,我们在学习的过程中注意区分。
下面用一个例子来说明面向对象编程的基本思想。
1.1. 用C来描述一个对象
我们以一个简单的例子来分析面向对象,这个例子是描述平面中的一个圆。这里我们需要两组参数来描述圆,第一个参数是坐标位置(X、Y);另外一个是圆本生的描述,例如大小(半径r),可能还有颜色等等,简单来说,我们目前只有半径。
如果从C语言出发,需要建立一个结构体来描述圆。
struct Circle
{
float x; // X坐标
float y; // Y坐标
float r; // 半径
};
上面只是定义了一个结构体(抽象,代表所有圆),但是还不存在一个真正的圆的实例(具体的对象),因此需要定义一个变量,其类型是Circle这个结构体。
struct Circle c1;
如果要改变c1这个圆结构变量的值我们有两种方法,首先是直接赋值。
struct Circle c1;
c1.x = 0;
c1.y = 1;
c1.r = 11.2;
这里通过点(.)这个限定符来区分结构体变量和结构体内的成员
另外还可以用函数的方式来改变结构体的值。
/**
* 给圆设定初始值
* @param c 圆结构的指针
* @param x 坐标X
* @param y 坐标Y
* @param r 半径R
*/
void setCircle(struct Circle *c, float x, float y, float r) {
c->x = x;
c->y = y;
c->r = r;
}
另外我们一次性的打印整个圆的数据,这样我们最好需要一个函数。
/**
* 打印
* @param c
*/
void printCircle(struct Circle *c) {
printf("The position x=%f, y=%f, radius=%f!\n", c->x, c->y, c->r);
}
把所有代码放在一起:
#include <stdio.h>
#include <malloc.h>
struct Circle {
int x;
int y;
float r;
};
/**
* 给圆设定初始值
* @param c 圆结构的指针
* @param x 坐标X
* @param y 坐标Y
* @param r 半径R
*/
void setCircle(struct Circle *c, int x, int y, float r) {
c->x = x;
c->y = y;
c->r = r;
}
/**
* 打印
* @param c
*/
void printCircle(struct Circle *c) {
printf("The position x=%d, y=%d, radius=%f!\n", c->x, c->y, c->r);
}
int main() {
struct Circle *c1;
c1 = (struct Circle *) malloc(sizeof(struct Circle));
c1->x = 0;
c1->y = 1;
c1->r = 11.2;
printCircle(c1);
setCircle(c1, 1, 1, 3.3);
printCircle(c1);
return 0;
}
1.2. C语言描述圆的总结
- 圆的结构体描述是一个抽象的描述,因为结构体是类型定义,并没有与其对应的变量;这儿描述和后面的Java类的定义很像。
- 声明一个结构体变量后就有一个真实的内存变量来描述一个特定的圆了,这个变量是圆(抽象描述的)一个实例(实体),也叫做对象;这个概念和Java的对象非常相似;
- 通过点(.)或者(->)可以操作结构体变量(对象)内部的成员,这点和Java对象的操作也非常相识,只不过Java都是用点(.);
这里我们发现,操作结构体的函数是独立定义的,并且需要把结构体变量作为参数进行传递。圆这个结构体是死的,需要外部的函数来对其进行操作。如果把圆这个结构体变量和生活中的对象联系起来,会有些有趣的事情。
例如,人这个对象有固有的特性(性别、年龄、身高、体重。。。)这些和圆的坐标和半径对应,都表示一个对象的特性;但是人可以说话,可以运动(动作、行为,相当于函数);而结构体变量的圆不能自己动作,只能依靠外部的函数进行动作。如果圆可以自己动作,例如让圆这个对象自己打印输出,这样不就可以和生活中的对象联系起来了?
struct Circle
{
float x; // X坐标
float y; // Y坐标
float r; // 半径
function void print(){
printf("The position x=%f, y=%f, radius=%f!\n", x, y, r);
}
};
struct Circle c1;
c1.print(); // C语言无法做到
如果要实现上面的功能,显然C是不能做到的,需要有更先进的语言,例如C++,或者是Java。
抽象来说,对象是一组相关数据以及在这些相关数据上操作函数的集合。
如果把结构体进行扩展,使其可以包含数据和函数,那么这个结构体就变成了面向对象中的类或者对象了。扩展结构体这个事情交给C++来完成,我们需要学习一个更友好、更方便的面向对象语言Java,C++对于大多人来说太难了,Java会简单许多。
当然面向对象不止有扩展结构体这样简单的功能,还需要学到相关的继承、多态、接口等高级特性,用来简化代码的编写。
1.3. 使用Java来实现
接下来我们使用java来实现上述的功能,这里主要是要把相关的函数作为结构体的一个部分。
/**
* 定义Circle类
*
* @author danny
*
*/
class Circle {
float x;
float y;
float r;
public void setCircle(float x, float y, float r) {
this.x = x;
this.y = y;
this.r = r;
}
public void printCircle() {
System.out.printf("The position x=%f, y=%f, radius=%f!\n", x, y, r);
}
}
/**
* 主类,主函数
*
* @author danny
*
*/
public class Circle_Test {
public static void main(String[] args) {
Circle circle = new Circle();
circle.x=0;
circle.y=1;
circle.r=11.2F;
circle.printCircle();
circle.setCircle(1, 1, 3.3F);
circle.printCircle();
}
}
看有什么区别?
- Java定义一个类是如何的?
- Java中13~16行中的this代表什么?
- Java的语法和C有什么区别?
- 注意C中33行和Java中32行代码,代表什么意思?
- 注意C中给结构体指针成员赋值与Java中对象成员赋值的区别。
- Java中类包含了函数,这是C中结构体不具备的能力。
- C和Java都有主函数Main,他们的写法有什么不一样?
1.3.1. 编译和运行Java
把Java代码保存到Circle_Test.java的文件名中。
命令行中执行:
javac Circle_Test.java
这两个class文件是java编译后的文件,类似Windows中的exe文件。但是这个文件不能直接运行,需要在Java虚拟机(VirtualMachine)中运行。
接下来使用命令行
java Circle_Test
运行刚刚编译后的代码,得到结果
The position x=0, y=1, radius=11.200000!
The position x=1, y=1, radius=3.300000!
这个结果和C的结果是一致的。
文件名必须是 Circle_Test.java,这个文件名的主文件名必须和源文件中 public class Circle_Test {...} 的类名保持一致。
javac是编译命令,java是运行命令(使用java虚拟机运行编译后的代码)。关于虚拟机下面会讲到。
2. JAVA的特点和历史
2.1. Java虚拟机
在我们学习Java之前,先来看看程序(应用)是如何运行的。
广义上来说,程序是运行在CPU上的一段二进制代码。为了使得CPU等硬件资源可以得到统一的管理,一般来说,我们需要一个操作系统(OS)。大多的应用程序(Application)都运行在操作系统上,这样多个程序可以在操作系统的协调下充分使用CPU等硬件的资源。当然,也有不需要操作系统的,例如嵌入式控制器(MCU)上面的很多程序因为功能简单,也不需要操作系统。
常见的操作系统有Windows,linux,Mac OS,Android等。不同操作系统上面的应用程序一般是不能互换的。即使同一种操作系统上面的程序因为CPU的架构(Architecture)不同,也不能互换。例如,Linux可以运行在X86(Intel/AMD的通用CPU属于这个架构)架构上,也可以运行在ARM(大多移动设备的CPU)架构上。但是一个为X86上Linux编译的应用程序一般情况下并不能运行在ARM上的Linux系统中(也有例外,例如Java的应用或者是有些跨平台的编程语言)。一般来说,我们把CPU架构和操作系统的整体称作平台(Platform)。
以前我们学过的C语言是一种静态编程语言(关于静态和动态编程语言的概念我们在后面讲解),需要一个编译链接过程后形成一个可执行文件,这个可执行文件就是通常所说的应用。如果你在Windows上面编译的C语言程序,显然是不能运行在Linux系统当中的,因为平台不一样。但是通常我们可以把源代码在目标平台上进行编译、链接(可能需要适当的源代码调整)使其可以在不同的平台上面运行。源代码这种可以通过少量修改的方法可以在不同平台下面运行的特性叫做可移植性。显然C语言是源代码级别的可移植。要做到编译后程序可以在不同的平台上运行就更好了,比如我们学过的Java就可以做到,因为Java不是直接在操作系统上面运行,而是在Java虚拟机上运行。Java虚拟机保证了所以编译好的Java代码都可以其上运行,而不用关心其平台是否一样。
好了,我们看到了一般的程序是如何运行的,如下图。
这种分层的设计理念我们在很多地方都用到,例如网络的TCP/IP协议层,软件系统当中的分层设计。分层的设计的目的就是为上层提供统一且稳定的服务而隔离下层的不同。从上图知,如果操作系统是Windows,无论硬件是Intel还是AMD,上层的应用都可以不做修改的运行(当然CPU架API: application programming interface JDK: Java Developing Kit IDE: Integrated Development Environment 构需要一致)。而Java的高明之处就在于Java自己有虚拟机,屏蔽了所有下层的不同,只要是Java的应用,在有Java虚拟机的平台上都可以运行。
2.2. Java的编译和运行
bytecode是java编译后的二进制代码,只能在java虚拟机上运行。
2.3. 基本概念
The Java Language Specification, API, JDK, and IDE
2.3.1. JDK、API、IDE
- API: application programming interface,应用编程接口。Java提供了一套最基本的类,包括文件操作、网络操作、数学运算、输出输出、数据结构等,用来简化你的开发。这些其实是最基本的数据结构和操作函数,只不过Java是按照类(class)的方式来组织,的;类比C语言中的标准函数库是按照头文件和C文件来组织的。
- JDK: Java Developing Kit,是Java的开发套件。类比C语言,可以看作是编译器、链接器这些命令行工具。不同的是,Java没有链接,只有编译工具(Javac);另外Java编译后的代码(.class文件,有叫做bytecode)不能直接运行,需要使用java虚拟机(java)运行。
- IDE: Integrated Development Environment,这是集成开发环境。一般人不会用写字板和命令行行(Jdk)来开发Java,因为效率太低了。
3. JAVA开发环境
使用命令行和文本编辑器也可以编辑和运行Java,但是效率很低,因此我们需要一个集成开发环境(IDE Integrated Development Environment)来编辑、运行和调试Java代码。
常用的IDE有 Eclipse,netbeans,IntelliJ IDEA 等。其中最强大的是 IntelliJ IDEA,但是其是收费的(有免费社区版本),为了统一,我们使用开源免费的 Eclipse 来作为我们学习的 IDE。
你可以使用任何IDE来开发Java,但是考试的时候只提供 Eclipse。因此掌握Eclipse是必须的。
3.1. 安装
Eclipse 是一个开放的框架,基于Eclipse框架的有很多不同语言的IDE,包括C、PHP、Python等;我们只需要下载Java的IDE就可以了。访问 Eclipse 官方网站 ,下载 "Eclipse IDE for Java Developers";你需要根据你的平台来选择。
最新的Eclipse已经自带了JDK,不需要单独下载JDK。
解压缩后双击eclipse.exe 就可以运行。
3.2. 运行
启动Eclipse,出现一个Workspace(工作空间)的选择。工作空间是一个目录,Eclipse在这个目录中可以建立多个项目(Project),每个项目可以包含多个java源代码文件。Eclipse在每次运行的时候会让你选择Workspace,这样Workspace->Project->Source code 就是Eclipse管理源代码的层级关系。
请选择一个你方便管理的目录作为Eclipse的Workspace,这个目录路径最好不要包括中文或者是特殊字符。
第一次打开空的Workspace会出现一个欢迎的界面,关闭就可以了。
除了菜单、工具条,Eclipse大约分为上述4个区间。
3.2.1. 新建项目
在左边有"Create a Java project"的快捷方式新建一个项目,也可以在File->New菜单中找到新建项目的功能。
在弹出的对话框中主要是:
- Project name:项目名称,建议使用英文,命名规则可以参考C语言的变量命名规则;
- Use an execution environment JRE:Java虚拟机的版本,请一定选择1.8这个版本,因为我们书中的例子是按照1.8版本的。
项目名称最好使用英文,符合C语言变量名命名规则。因为项目名称直接对应到Workspace文件夹中的一个子文件夹,中文可能会造成意想不到的问题。
在开发过程中请记住,不要使用中文来命名任何的名称,保持C语言变量名规则是一个好策略。例如一般只有注解、需要输出或者是显示的地方使用中文。
生成项目后的界面如下:
这个界面中项目名称不用说了;JRE System Library 是虚拟机提供的库,类似C语言中提供的标准库(文件、算术、网络库等),这些库中的类(C语言中是头文件)是可以在我们源代码中引用的;src是一个特殊的文件夹,所有的Java源代码必须在这个文件夹中。
3.2.2. Hello world
我们建立一个简单的Hello world 程序,这里会使用到包的概念。
对于简单的程序,可能一个源文件就可以了,例如编写一个Hello world的代码。但是对于复杂的应用,一个文件显然有很多缺点(为什么)?考虑一下C语言是如何管理多个源代码的工程的?
把一个复杂的工程拆分成多个小的代码片段可以便于工程项目的管理与维护,当然编译速度也会加快。但如果所有源代码放在一个文件夹中还是非常混乱,因此Java引入了Package(包)的概念。我们先界面左边src上单击鼠标右键,在菜单中选择New->Package新建一个包。
源代码位置一般不要轻易修改,只需要填写包名。
注意这里又是名称,记住我们上面的规则
现在左边src下面多了一个first的包。现在鼠标右键单击first,菜单中选择New->Class
- Name:类名,上面名称的规则。在Java中,一个类一般是一个文件(一个文件可以包含多个类,一般不建议)。一个类的定义如同C中结构体类型的定义。Java推荐类名的首字母大写;
- Modifiers:可视等级,这个以后解释,这里选择 public 就可以了;
- Superclass:父类,这个以后解释,保持缺省;
- public static void main(String[] args):如果C语言需要一个main的主函数作为启动入口,如果你需要,请勾选,会自动给你建立一个空的main函数。因为我们需要这个类可以运行,因此勾选。
完成后,会打开一个新的类:
package first;
public class Hello {
public static void main(String[] args) {
// TODO Auto-generated method stub
}
}
接下来我们修改这个类框架,主要是修改main函数部分,结果如下:
package first;
public class Hello {
public static void main(String[] args) {
System.out.println("Hello world!");
}
}
保存(ctl+s)后,可以运行看看结果:
3.2.3. 包和源文件结构
我们看看文件系统中的情况,如上图。包其实对应到的就是文件夹。当然一个包中可以再包含子包,如果一个文件夹中可以包含子文件夹一样。
类名永远是和文件名对应的,类名是Hello,对应的文件就是Hello.java
注意,源代码的第一行是:
package first;
表明了这个文件是属于哪个包的,那么可以在src下面建立一个类吗?答案是可以的,不过这里不用新建,我们使用鼠标在左边的工程列表中把已经建立好的Hello.java拖动到src目录这个位置,Eclipse会新建一个同名的文件。但是这个文件在哪里?
这个文件并不在src文件夹中,而在一个叫做 default package 的包中,我们打开这个文件:
public class Hello {
public static void main(String[] args) {
System.out.println("Hello world!");
}
}
注意,第一行并没有 package 这个申明。在看看文件系统:
Hello.java 这个文件的确在src文件夹中。
src文件夹中的类文件的package可以不用写,代表在缺省包中,也就是src这个文件夹中。
如果需要改变源文件的位置,请在Eclipse中的项目管理中进行(支持文件的复制、移动、删除等操作)。Eclipse始终会保持源代码中的package申明的正确性。这个功能其实在高级的IDE中叫做重构(Refactor),重构还有很多应用,例如批量修改函数名、变量名等功能,可以在菜单 Refactor 中找到更多的功能。
3.3. Java代码结构
总的来说,Java是一种类C的语言,基本语法和C十分相似。例如函数、变量名命名规则,代码块,判断、选择语句,循环语句,注解等都保持和C一致。
我们再来回顾以下上面最简单的Hello world:
package first;
public class Hello {
public static void main(String[] args) {
System.out.println("Hello world!");
}
}
- package:包描述,该源代码在那个包,对应src文件夹中的字文件夹。如果有多个层级的包(src子文件夹中的子文件夹),使用点(.)来分割包名。尝试一下在包下面建立子包后package是如何描述的。package不是必须的,如果java文件在src中,package可以省略,这时表示缺省包。
- 一般来说,一个Java文件包含一个类(Class)结构,如果有多个并列的Class结构,只能有一个描述成public。关于public的含义后面会讲到。
- 类名(或者描述成public的类名),这个例子是Hello,必须和主文件名一致。
- Java中的变量、常量、函数等结构都属于一个类(Class),在类外面定义非类(Class)的结构都是非法的。
- 如果要代码运行,需要一个主函数main,且这个函数的写法是固定的(上面代码第5行),且main函数所在的类必须是public修饰的。
- 一个工程中可以有多个类,多个主函数,这个和C是有区别的。C中一个工程只能有一个主函数。
后续的学习中会学到更多关于语法方面的内容,这里只做一个初步的了解。
3.3.1. 关于指针
这里讨论指针比较早,C中的指针是一个非常重要的概念,在Java中关于类似C指针的概念有区别,这里需要进行说明,请大家先有个印象。
- 首先Java中不存在C中指针的术语,与之对应的是引用(reference)。引用和指针其实是相似的;
- Java中有动态分配内存(如使用 new 操作符),但是不需要回收,Java虚拟机会自动回收;
- Java中有引用的操作(类似C中取指针地址)也是使用(&)操作符。
关于引用的具体内容我们放在函数和类中进行讲解。
4. 错误类型
错误类型包含三种:Syntax Errors、Runtime Errors、Logic Errors。
4.1. Syntax Errors
语法错误,编译的时候发现,例如:
public class ShowSyntaxErrors {
public static main(String[] args) {
System.out.println("Welcome to Java);
}
}
4.2. Runtime Errors
运行时错误,运行时产生的错误,例如空对象操作、0作为除数、文件不存在等。运行时错误是我们需要特别关注的,因为会导致程序的流程异常,产生意想不到的结果。例如下面这个例子:
public class ShowRuntimeErrors {
public static void main(String[] args) {
System.out.println(1 / 0);
}
}
运行时错误也叫做异常(Exception),后面会专门讲到异常处理。
4.3. Logic Errors
逻辑错误,简单说来就是语法没错、也没有运行时错误(异常),但是结果错误了。通常是编程者的算法错了,例如:
public class ShowLogicErrors {
public static void main(String[] args) {
System.out.println("Celsius 35 is Fahrenheit degree ");
System.out.println((9 / 5) * 35 + 32);
}
}
你认为上面代码的结果是什么?
4.4. Debug
debug功能非常重要,虽然考试不会直接考,但是debug会极大的加快编码调试的效率,请同学们一定重视。
基本的调试非常简单,就只有端点设置与变量查看,再掌握几个快捷键会提升你调试的效率。下面我们在缺省包的位置建立一个简单的循环打印代码来演示调试的技术。
代码如下:
public class Debug {
public static void main(String[] args) {
String outStr = "";
for (int i = 0; i < 10; i++) {
outStr += i + ",";
System.out.println(outStr);
}
}
}
输出结果是:
0,
0,1,
0,1,2,
0,1,2,3,
0,1,2,3,4,
0,1,2,3,4,5,
0,1,2,3,4,5,6,
0,1,2,3,4,5,6,7,
0,1,2,3,4,5,6,7,8,
0,1,2,3,4,5,6,7,8,9,
4.4.1. 设置中断
在需要中断(程序会在进入这一行之前暂停)的源代码行号前面双击,会出现一个蓝色的点,表示中断以及设置成功,接下来点击调试按钮就可以让程序运行,并且在设置中断的地方暂停。
4.4.2. 调试界面
eclipse的调试界面是单独的布局,第一次运行调试会提示进入调试界面,单击switch按钮就可以了。运行调试后出现下面的界面:
- 中断行:高亮显示,表示当前需要执行的行(还未执行);
- 调试窗口:当前的变量值;断电设置;表达式计算等;
- 输出窗口:和运行是一样的,不过这个输出要更具代码的具体执行位置进行输出;
- 运行按钮:控制代码执行的按钮:
- Step over(F6):单步执行(跳过),运行到当前下一行代码,如果有函数,不进入函数;
- Step into(F5):如果高亮代码有函数,进入函数执行(单步执行);
- Step return(F7):从函数中返回到被调用位置(单步执行);
- Resume(F8):执行直到下一个个中断位置;
- Terminate(Ctl+F2):终止运行,无论是否运行完成,立即终止并退出。
尝试不断按F8,观察调试窗口、输出窗口的变化。
在代码编辑区域,用鼠标指向变量,会显示变量的当前值。
调试还有很多高级功能,例如条件中断(在一个断点上设置一定的条件,不是每次都中断;表达式计算等等),大家可以尝试一下。用好调试工具可以极大提升学习的效率。
当程序成功运行并退出,Terminate按钮会变成灰色;如果是红色,表示程序仍然在运行。
调试完成后,可能你想要回到以前的编程视图,这时需要单击右上角的视图切换(Perspective)。
5. 小技巧
为了方便我们的编程,以下两个小技巧是最常用到的。
5.1. 让Eclipse任何时候都进行提示(代码补全)
代码补全是一个非常好用的工具,当你输入一个字母的时候,IDE会给你提示,这样可以加快编码,同时减少出错。可是缺省情况下,Eclipse只在键盘输入点(.)的时候进行代码提示。如果需要输入任何字母的情况下提示,可以按照以下方法进行设置。
菜单:Window->Preference 打开Eclipse偏好设置,如下图,找到Java->Editor->Content Assist,在右边的这个地方点的后面输入26个英文字母(小写即可),点击 Apply and Close 就可以了。
5.2. 代码格式化
菜单 Souorce->Format 或者快捷键 Ctl+Shift+F ,可以使当前代码快速美化。漂亮的代码缩进和编码规则会让代码更容易阅读。
不同系统下 Eclipse 的快捷键可能并不相同,注意参考你自己Eclipse的快捷键。
6. 本章节重点
- JDK、API、IDE: P11-1.6
- Java语言的基本结构:P12-1.7
- 建立、编译、执行Java:P15-1.8
- 错误类型:P20-1.10
补充内容:
- 包和文件夹的关系;不考,但是掌握后有帮助。
- Eclipse的使用,特别是调试(Debug);不考,但是非常有用。