系列文章目录
第一部分——编程基础与二进制 1
第一部分——编程基础与二进制 2
文章目录
第二部分——面向对象
3.类的基础
3.1类的基本概念与面向对象
这里我不打算按照书上介绍给初学者那样的方式来讲述类和面向对象的概念,下面的描述也许有一定的理解之后看起来更能加舒爽
3.1.1抽象
我们先提一个词,抽象。这个词我认为很重要,在计算机相关学科中会不停的出现这个词,计算机网络七层网络模型是对互联网信息传输的抽象,操作系统是对底层硬件的抽象,数据库是对文件信息存储的抽象,甚至于我们说我们上一章着重讲的数据类型依然是对计算机底层二进制数据的抽象,我们不需要关心这一串01序列存储的顺序,我们不需要关心怎样表示正数怎样表示负数,我们将地球上更少数人能理解的东西通过人脑思维抽象成更多数人能理解的东西,我们规定这样一串4个字节01序列组成的整体是一个整数int,它可以运算,可以比较,可以干很多复杂的过程,这个过程就是抽象
3.1.2类与实例
当我们嫌弃高低电平的离散化数据很麻烦的时候,我们抽象出了01信号;当我们嫌弃01信号的组合太臃肿麻烦的时候,我们抽象出了指令,抽象出了基本数据类型,同样当我们觉得一个或多个数据类型来描述我们想要描述的事物显得太过松散的时候,我们需要一个更高层的抽象,一个学生的成绩单可以是(”张三“, 90,80,96,69)的序列,那为什么不能只是一个学生的成绩单Grades,这个时候类的概念逐渐有了模型,它是我们对纷繁复杂由数据组成的世界的更高层次的抽象,我们认为在这个学校这个学期所有学生的成绩单都有着这样的一个总体特点,就是它们都是由一个学生的人名和它四科成绩组成,而每一个不同的成绩单就正好是这个抽象个体的具象体现,有人叫张三,有人叫李四,但是都没有关系,你们的成绩单都属于这一类,不会因为你叫不同的名字就会比别人多一科或少一科成绩,这就是类与实例的关系
3.1.3面向对象编程
有了类与实例的理解,我们再来回顾我们的编程过程,当你已经理解了抽象的含义,理解了类同实例的关系,你会发现,编程的方式发生了微妙的关系,我们在试图描述这个抽象的类,一张纸质的成绩单可以告诉我你叫什么名字考了多少分,那么我想一个成绩单类可能也能在你提出想要的信息的时候返回给你;一张纸质的成绩单可以让你最直观的感受这四科分数的平均分,那么我想一个成绩单类也可以计算得出相应的信息返回给你,而这个你不需要关心事物的运行过程,只因为你相信它可以做所以你能直接用的编程方式就是面向对象编程。
在一个面向对象编程的世界里,自然而然地存在一种上层对下层模块地信任,信任它能给我我想要地东西;也存在一种下层对上层模块地责任,我能通过我所掌握地信息给你你想要地反馈,因此越往抽象层的顶层去,你需要关心的方面就越集中。这样讲很抽象,那我来举一个例子
比如今天我想开发一个五子棋游戏,我们来对这个系统进行面向对象建模。
最直观的,五子棋有棋子,因此有这么一个chess类,它存储着必要的信息,如这个棋子的颜色和它的x,y坐标
然后就是承载五子棋的棋盘,它也存储着它必要的信息,如这个棋盘上所有的棋子,这个时候对于棋盘来说,它不需要关心这些棋子各自的颜色和x,y坐标,因为它相信当我需要用到这些元素的时候我传入这个棋子,它就能告诉别人它自己的颜色和坐标,对于棋子来说,它不需要关心上层会怎么使用我的颜色和坐标,我只需要提供记录好这些信息就好了。同时,棋盘理应还有一个展示自己图像的方法,它利用提供给它的图形API,根据所拥有的棋子的信息,绘制当前棋盘的状况,我也不知道什么时候要调用绘制自己的方法,我只用知道,当有人调用我这个方法的时候,我能准确的利用我拥有的信息画出来就可以了
然后就是这个游戏本身,它需要记录现在在下棋的到底是哪两个人,他需要记录这个游戏现在是谁在落子,他需要记录当前的时间,这些就是这个类的属性,同时它需要掌握它的规则,有一个方法叫落子,给出落子的坐标和当前落子的人,它就更新这局游戏棋盘实例的棋子列表,然后调用绘制棋盘的方法,然后来判断这次落子之后游戏有没有结束,它就不需要考虑鼠标的点击位置,不需要知道点在格点之间的坐标到底算哪个格点的,它只用提供给更上层的抽象一个可以更新棋局的方式,同样它也不需要关心我调用了更新棋盘的方法棋盘以何种方式绘制和更新,存不存在渲染效率的问题等
当然讲到这里还可以继续讲,不过我相信读到这里,你也许或多或少的触碰到了这种”机器人“式的思维,而面向对象的编程就是这样一个同过层层抽象分层次地解决问题地过程
3.2类的语法
3.2.1类的定义
- 类的信息:限定描述符 + 类名
- 属性:访问修饰符 + 数据类型 + 属性名称 + (默认值)
- 方法:访问修饰符 + 返回值类型 + 方法名 + 形参列表 + 方法体
public class Point {
public int x;
public int y;
public void print() {
System.out.println(x + " " + y);
}
}
其中,一个小tips,一个java文件里面可以拥有多个class,但是只能有一个class被public修饰,同时这个被public修饰的类名需要和.java文件的文件名一致
上述具体的限定描述符以及具体细节后续会专门设立部分讲述,这里知道存在这些组成部分即可
3.2.2类的创建
- 构造方法:一种特殊的方法,用来初始化这个类,形式表现为没有返回值且方法名与类名一样
- 创建的语法:new关键字
Point point = new Point();
3.2.3类的使用
通过 . 运算符可以访问类的可访问的属性,调用可以调用的方法
Point point = new Point();
int x = point.x;
int y = point.y;
point.print();
3.3包的概念与语法
3.3.1包的概念
Java中使用包的概念解决命名冲突问题,同时Java中也是通过包的方式管理类,形象的理解包就像文件夹一样,我们将类与接口们放在一个个不同的包下面,这些包可能存在着层级,我们同样可以通过 . 的方式来访问,如String字符串类就在java.lang包下,而一般java程序会默认导入这个包下的所有类,因此我们在访问时可以直接访问
当我们将一个类同类名和包名一起表示出的时候,如java.lang.String,这个就乘坐这个包的完全限定类名
3.3.2声明类所在的包
通过关键字package定义包名
package com.moozlee;
public class Hello {
//类的定义
}
形如上,如果源文件的根目录为D:\src\,那么这个Hello.java文件应该被放置在D:\src\com\moozlee\目录下,而在java项目的开发中一般我们会使用域名倒置+项目名称的方式定义最上层的包,如今天我的域名时www.moozlee.com,那么我开发的在这个网站上运行的云工厂项目报名就会是com.moozlee.factory,这个习惯是为了和其他开发者定义的包不冲突而形成的
3.3.3通过包使用类
前面提到过,java会默认导入java.lang包下的类,但是除开这个包,当我们需要访问非本包下的类时,直接使用会报错,此时我们就需要通过完全限定类名或者导包内类的方式使用
int[] arr = new int[] {1, 4, 2, 3};
java.util.Arrays.sort(arr);
这里就是通过完全限定类名的方式使用了java.util包下的Arrays类,显然这样使用过于繁琐,因此我们可以使用import关键字:
package com.moozlee;
import java.util.Arrays;
public class Hello {
public static void main(String[] args) {
int[] arr = new int[] {1, 4, 2, 3};
Arrays.sort(arr);
}
}
上面就展示了如何使用import关键字导入一个类,如果我们需要的话我们也可以通过import关键字一次性导入多个类如:
import java.util.*;
这样我们就导入了java.util包下面的所有类,但是这个导入过程不能递归,它不会导入java.util包下子包内的类,如java.util.zip包下的类就不会导入
有一种特殊类型的导入,名为静态导入,多了一个关键字,static,这个关键字我们后续会细讲,这样可以导入类下的public static修饰的方法和成员,如下:
import static java.lang.System.out;
public class Hello {
public static void main(String[] args) {
int[] arr = new int[] {1, 4, 2, 3};
Arrays.sort(arr);
out.println(arr);
}
}
3.3.4双亲委派机制
我们这里可以思考一个问题,如果我故意定义一个与官方库提供好的类,然后修改这些官方api达到破坏的目的怎么办的?
首先我们定义一个自定义一个Math类,这个类在java.lang包下,是java中用于处理一些数学相关计算的一个类,有一个静态方法可以用来判定两个数中的最大值
package java.lang;
public final class Math {
public static int max(int a, int b) {
System.out.println("自定义的Math类");
return (a >= b) ? a : b;
}
}
比起官方定义的这个函数,我们多了一句打印,然后我们在另一个包下面的测试类测试使用
package com.moozlee.core;
public class PackageTest {
public static void main(String[] args) {
System.out.println(Math.max(1, 2));
}
}
很显然,官方api不会这么不堪一击,这里使用的仍然是官方的api,那么这里对于同样包名同样类名的Math类,我们是怎么保证导入的是官方api中的那个类呢?
这里就要涉及jvm对于编译好的class文件的导入细节了,在jvm中加载类需要用到一个重要的组件叫做类加载器ClassLoader,在java系统里我们支持了四种类加载器分别如下图所示:
这四种类加载器都各有分工,负责加载不同类型的class文件
- Bootstrap ClassLoader ,主要负责加载Java核心类库,%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。
- Extention ClassLoader,主要负责加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。
- Application ClassLoader ,主要负责加载当前应用的classpath下的所有类
- User ClassLoader , 用户自定义的类加载器,可加载指定路径的class文件
而对于我们上面提到的官方api的Math类就是由最顶层的BootstrapClassLoader负责加载的,而我们自定义的Math类则是由AppClassLoader负责加载的,那么具体加载规则又是怎么样的呢?
当我们需要加载一个类时,如果没有自定义类加载器(有自定义类加载器后后续处理逻辑一样),首先会直接请求AppClassLoader加载这个类,AppClassLoader在接收到请求之后,不会直接去加载,而是先检查虚拟机是否已经加载过,如果有直接结束;如果没有就会委派给ExtClassLoader,同样ExtClassLoader也不会直接加载,也会先检查再委派给BootstrapClassLoader,由于BootstrapClassLoader已经是最高级的了,因此接收到委派且发现这个类没有被加载后就会去自己负责加载的范围中寻找,找到了就加载,结束流程,没找到就会抛出异常,ExtClassLoader就会捕获异常,然后再在自己负责的范围中去寻找,找到了就加载,没找到也会抛出异常,由子加载器捕获,依次向下,当所有的类加载器都没有找到这个类时,就说明不存在这个类,会将ClassNotFoundException直接抛出报错
知道这个机制后,我们再看上面的例子,对于java.lang.Math这个类,即使它已经存在在了AppClassLoader的空间中,因为不会直接加载被委派给了上层的加载器,而当到达BootstrapClassLoader这一层时,就会直接加载官方库中的Math类,直接退出流程,因此我们自定义的类无法被加载进jvm,自然就无法运行
同样,这种双亲委派机制也是可以且需要在某些场景下被打破的,具体的可以自行查阅《深入了解java虚拟机》
3.3 类中的关键字们
3.3.1类修饰符
-
访问控制修饰符
对于普同类来讲,只有两种访问修饰符,public或者不加修饰符也就是默认的访问级别
public class可以被所有其他类访问和引用,而默认不加修饰符的类只具有包访问性,即只能被本包内的其他类访问和引用
但是一个类的内部类可以被其他访问控制修饰符protected、private修饰,相当于类的成员,同类的成员由一样的访问特性,具体关于内部类后续会详谈
-
非访问控制修饰符
-
abstract
使用abstract关键字修饰的类是一个抽象类,抽象类无法被实例化需要被继承后才能使用,后续关于抽象类会同接口一起详谈
-
final
当一个类被final修饰时说明这个类将无法被继承,继承将会在下一章详细解释,无法被继承的直接好处这个类所有方法的实现都是固定的,不会被修改的(当然也有方法可以破坏),如String,Socket等类都是final的,且final关键字无法与abstract关键字一起出现
-
3.3.2成员变量的修饰符
-
访问控制修饰符
-
public
同类中public关键字一样,类中的这个属性可以被其他所有类访问,不受限制
-
protected
protected关键字修饰的属性,满足以下两个条件之一就可以被访问:
1.与此类同包,也就是同一个包内的其他类可以访问这个类中被protected修饰的属性
2.是这个类的子类,也就是子类可以访问父类被protected修饰的属性
-
default
首先声明一点,java中访问修饰符没有default这个关键字,这个关键字另有它用,因此使用default修饰属性是编译不通过的,这里是为了表示不写访问修饰符的默认状态
-
private
最不开放的关键字,只允许本类直接访问,不允许其他类包括子类
-
-
非访问控制修饰符
-
static
这里是第一次出现static这个关键字,之后会出现在很多个地方,感性的记忆就只用记住所有static关键字表示的字面含义就是被static修饰的无论是属性还是方法还是代码块都是属于一个类的,因此同一个类的所有实例对象访问的都是一个属性值,读是读的同一个值,写也是也的同一个值;理性的理解就只用记住static关键字修饰的属性存储在一个公共空间中(JDK8以前在方法区,JDK8删除了永久代就被迁移到了堆中),同时对于static关键字还有一点需要记住,static修饰的无论属性、方法、代码块都是在类被加载的时候加载进入内存(静态代码块是这个时候运行,另外两个是加载),这个对于后面理解为什么静态方法不能访问普通属性而静态代码块可以有帮助
-
final
final第二次出现,当final用来修饰成员变量时,说明这个成员变量是常量,无法被修改,这里的无法被修改需要注意一点,如果对于基础数据类型如int,float,char等无法被修改就等于值没有办法被修改,而对于引用类型的变量,如final User user = new User()这种来讲,无法被修改指的是user这个引用无法被修改指向其他值,但是user内的非final修饰的属性是可以被修改的
同时,我们需要知道final修饰的成员变量,相比较于普通变量赋值的方式会少一些
普通成员变量:
- 默认初始化
- 显式赋值
- 代码块赋值(几乎不用)
- 构造器赋值
- 通过 .属性或 .方法赋值
对于final修饰的成员变量:
- 显示赋值
- 代码块赋值(几乎不用)
- 构造器赋值
因此,对于final修饰的常量,要么我们在声明时就显示的赋初始值,要么就需要在构造函数里面赋值
-
volatile
用来声明这个变量的可见性以及禁止指令重排,这个关键字后续在juc的系列文章中会详细讲述
-
transient
声明变量为一个暂时性变量,直接影响是当类实现Serializable接口企图被序列化的时候,transient修饰的变量将不会被序列化,但是如果实现的是Externalizable接口,序列化的逻辑由writeExternal方法决定,不会受transient关键字影响
-
3.3.3方法的修饰符
-
访问控制修饰符
概念同成员变量一样
-
非访问控制修饰符
-
abstract
-
用于在抽象类中指定抽象方法,抽象方法没有方法体,需要被继承抽象类的子类实现,如果子类没有实现则子类也需要被定义成抽象类
-
一个抽象类可以没有抽象方法,但是一个抽象方法的类一定要是抽象类
-
abstract修饰的方法不能是private修饰的,jdk1.8之前抽象方法默认为protected,jdk1.8之后默认为default
-
-
static
静态方法,可以直接通过**<类名>.<方法名>**的方法使用这个方法,且静态方法中不能使用成员变量
-
final
第三次出现,对于方法而言,final修饰的方法意味着不能被子类重写,所有被 private 修饰符限定为私有的方法,以及所有包含在 final 类中的方法,都被认为是final方法
-
native
说明这个方法的实现是由其他语言如c c++等来实现,多出现在jdk的一些底层api上
-
synchronized
用这个关键字声明的方法在被执行的前后都会申请一把锁,保证线程安全性(static方法申请的就是Class对象,实例方法申请的就是对象实例),具体将会在juc系列文章中详述
-
3.3.4访问控制修饰符的总结
访问级别 | 访问控制修饰符 | 同类 | 同包 | 子类(不同包) | 不同包(其他类) |
---|---|---|---|---|---|
公共 | public | 允许 | 允许 | 允许 | 允许 |
受保护 | protected | 允许 | 允许 | 允许 | 不允许 |
默认 | 缺省修饰符 | 允许 | 允许 | 不允许 | 不允许 |
私有 | private | 允许 | 不允许 | 不允许 | 不允许 |