Java面向对象
1.面向对象入门
我们知道Java是面向对象的一门语言,我们现在开始正式进行学习面向对象。
我们学习过 Java 基础语言后会知道,在 Java 中对象其实就是一个类class
在 Java 世界中所有的程序都是由 Class(对象、类) 组成的。为了更好的理解对象这件事,在编程世界里有一个专门的图形表示的语言来管理和维护对象,这个语言就是 UML 。
UML 提供了一套图形标准,通过使用 UML 可以让编程人员能够阅读和交流系统架构图、设计图,就好比建筑工人看施工图来施工一样。
我们可以把对象转化为图形,看图会比看代码更容易理解,所以想要理解面向对象,我们会先从 UML 图形开始。
请注意,UML 不是只针对 Java 语言的,它适合于所有编程语言,所以学会这个是一件非常棒的体验。 掌握 UML 技术对于掌握面向对象是个非常大的助力,因为初学者一般来说都比较缺少抽象能力,而面向对象的核心工作就是抽象和封装,我们借助图形可以帮助我们训练抽象能力。
UML图举例:
转化成Java代码如下:
package com.qq.model;
import java.util.ArrayList;
/**
* 群对象,封装了群的基础数据
*/
public class Group{
//群唯一的ID
String id;
//群号
String groupNo;
//群图标
String icon;
//群名称
String name;
//群公告
String notice;
//群简介
String desc;
// 群成员
ArrayList<User> users;
}
我们现在简单了解了UML,可以简单看看,我们后面用的多,不着急
1.1 类的相关
接下来我们就要结合 UML 类图来逐步深入了解面向对象。
那是因为 Java 是面向对象的语言,在真实的世界里,所有的事物都可以用对象来表示,比如说学校、医院、商场、电影、音乐等等这些都可以用计算机来模拟出来,我们一般会把这些事物称为领域。
继续说面向对象这件事,对象是对问题领域中事物的抽象。
抽象就是把事物变成编程语言的过程
就以上面那个为例
对象是group,里面的所有是属性。
特点:万物皆可对象,每一个对象都是唯一的,具备属性和方法。我们对于他们后续会有了解。
1.2 面向对象-抽象
在 Java 面向对象中,我们可以把上述的抽象过程视为面向对象编程,在 Java 中一切皆对象,任何事物都可以抽象成一个 Java 对象
创建对象:
现在我们开始尝试一下自己创建 Java 类:
我们现在以LOL为例:
我们需要设计英雄,我们先建立一个英雄类:
但是要注意四个规范:
Java文件名由类名+.java构成
类名遵循大驼峰命名法,就是首字母大写,另外类名里不能有中文和特殊字符哦(比如:&、#、@、* 等),只能是字母和数字。比如: HelloWord、Test、Test40
遵守固定格式进行命名
public class 类名称 {
}
具体来说就是这个
public class Hero {
}
我们建立时候右键建立java class里面建一个类就可以了
创建好文件,回车后会打开该文件,系统已经自动创建好Java文件啦
1.3 包
由于 Java 天生是面向企业级的编程语言,所以基于工程化的考虑,一般不会把所有的 Java 类都存放在默认的包下,这样文件会非常难于管理,我们在开发的时候就一般采用package来管理代码
包的路径:我们在Java创建的时候自定义文件夹,就是包路径,例如:
package com.java.LOL;
现在我们说一下包名的规则,一般来说包名的开始是一个公司或者一个组织的域名的反写,如果是个人也可以遵守类似的规则
就像com.java.LOL一样
创建好的代码是这样的:
package com.java.LOL;
public class Hero {
}
我们多了一个包路径的语法,包路径是该 Java 所在的包目录。一个文件只有一个 package 语句,并且一般是在文件第一行
我们需要自定义导入类的情况,因为我们会需要引入别的类,所以我们即将使用import来导入类.
我们在打英雄联盟的时候会有武器,我们引入了一个weapon类,需要导入,所以我们就采用以下方法进行导入:
import com.java.LOL.Weapon;
正如你所见,不管是系统创建的还是我们自己创建的类对象,我们 import 的时候写上正确的完整类路径就可以了
import 语句得在 package 之后哦,这个顺序不能错了
1.4 属性和方法
我们现在在英雄联盟里面建立好了英雄和武器对象,我们需要在里面添加相应的属性需要了解英雄的特征
我们现在给英雄添加姓名属性,血量属性,物理攻击,魔法攻击属性与等级。
我们像这样添加对象属性:
public class Hero {
public String name;
public int blood;
public Weapon physics;
public Weapon magic;
public int level;
public int hurt;
}
我们甚至可以定义好属性
public class Hero {
public String name = "亚索";
public int blood = 120;
public Weapon physics;
public Weapon magic;
public int level;
public int hurt;
}
这样是可以的
我们在对象名之有访问限制词public,另外还有protected和private属性,我们后面会慢慢说明
对象属性定义时我们可以直接赋值
对象方法:在创建对象时,我们可以走了吗添加方法,表达对象可以进行的操作例如:
public class Hero {
public String name;
public int blood;
public Weapon physics;
public Weapon magic;
public int level;
public int hurt;
public int remainBlood(int blood,int hurt){
blood-=hurt;
return blood;
}
}
我们在里面定义了英雄的剩余血量的一个计算方法。
我们发现不同于其他的是我们没有使用static关键字进行修饰,因为我们是对象方法,不是静态方法,所以我们不需要static进行修饰,但是在有main方法里面的必须使用,不能在静态方法里面调用非静态方法。
例如:
public int remainBlood(int blood,int hurt){
blood-=hurt;
return blood;
}
我们现在完成了对象属性建立的基础,我们可以进阶下一步了
1.5 实例化对象
我们知道英雄现在目前还不能使用,现在英雄还处于一个抽象的状态,我们现在必须实例化这个英雄。
实例化方法如下:
public class test {
public static void main(String[] args) {
Hero hero1 = new Hero();
Hero hero2 = new Hero();
Hero hero3 = new Hero();
}
}
我们实例化了以后才可以调用里面的方法:
hero1.leval(100);
hero2.remainBlood(250,70);
把对象添加属性赋值以后:
hero1.name = "亚索";
构造函数:实例化的语法是new 构造函数();
实际上大家只要记住关键信息即可。你只需知道构造函数是其实一个比较特别的对象方法,具体表现在:
名称和类名一模一样,方法没有返回值,如果不写,则系统默认生成一个。
构造函数一般配合实例化一起使用的,你可以把构造函数当作类对象的初始化方法。
请注意,构造函数是对象方法,所以也具备所有方法的能力,比如有参数、可以在方法里写代码逻辑。
实例化背后默默的执行了一些方法。
1.6 构造函数
hero1.leval(100);
hero2.remainBlood(250,70);
hero1.name = "亚索";
你们发现这样写有点麻烦,我们可以采用构造函数方法进行执行:
public Hero(String name){
this.name = name;
}
this相当于Hero新new的对象名
我们就可以这么实例化了:
Hero hero1 = new Hero("亚索");
我们如果定义了无参数的构造函数,默认的就失效了
这时候我们再添加一个无参的就ok了。
public Hero(){
}
public Hero(String name){
this.name = name;
}
2. 对象的特征
对象具有封装性,继承性和多态性。
封装则是把对象属性封装在一个类里面。
我们来看看其他吧。
2.1 访问权限修饰符
面向对象的基本思想之一是封装实现细节并且公开接口,在 Java 语言中采用了访问控制修饰符来控制类,以及类的方法和变量的访问权限,从而达到隐藏实现细节只暴露接口的能力。
这个相当于一个权限,试想一下,就把你这个人看作一个对象,你是不是有自己的秘密不肯公开,是不是有什么心里话想对自己的朋友说,是不是有什么新鲜事想公开说,是页源代道理。从此以后我们就用这些权限来说事了。
我们有4个关键词来修饰权限。
public公开、protected保护、default默认、private私有
修饰符 | 同类 | 同包 | 不同的包 |
---|---|---|---|
public | 有权限 | 有权限 | 有权限 |
protected | 有权限 | 有权限 | 没有权限 |
default默认 | 有权限 | 有权限 | 没有权限 |
private | 有权限 | 没有权限 | 没有权限 |
我们了解以后,可以具体看看实际操作
类:我们在创建类的时候,可以选择public和默认两种,我们一般情况都是public
例如:
public class Hero {
}
或者:
class Hero {
}
类的方法与实例变量:
我们以前就说了。
现在我们升级以下英雄类,进行改造。
public class Hero {
private String name;
private int blood;
private Weapon physics;
private Weapon magic;
private int level;
private int hurt;
}
注意一下:protected 一般是和父子类在一起才生效的。
2.2 封装
当我们了解了访问控制修饰符后,我们可以开始学习面向对象最基础的能力:封装
封装简单的来说就是把对象的属性隐藏起来,只能通过公开的方法来得到或者设置属性值。不过呢,目前业界已经形成了统一的方法规范,那就是 getter、setter 规范。
刚刚我们把英雄类访问属性变成private以后是不是发现在不同类里面实例化就报错了,我们要这么做采用getter setter的方法进行访问比如说:
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
在idea里面可以alt+insert选择getter and setter即可
格式:get+属性名称(属性名的首字母要大写),用于获取属性
set+属性名称(属性名的首字母要大写),用于设置属性
我们建议先自己自写以后再快捷键
this方法:
Java 类进行实例化后,每一个实例都是唯一的,同样类的成员变量(实例变量)也是唯一的。这个变量的内存空间会随着类实例的创建而被创建,随着类实例的销毁而销毁,叫做垃圾回收。
有没有发现我们设计有一个this,this就是类被实例化对象,通过this可以调用实例里面的成员和方法。
我们发现setName里面方法参数名称和成员变量名一模一样
这点可能让你产生困惑了,没有关系,请记住由于 this.name 和参数 name 在不同的作用域所以可以用相同的名称。
除了实例内的作用域外,还有一个作用域是方法内,方法内的对象名称是不能重复的,并且方法内的变量是不能用修饰符的。
封装可能有点抽象,我们可以看看UML图来进行实现。
我们多写几个就好了。看看一个例子:
package com.java.LOL;
public class Hero {
private String name;
private int blood;
private Weapon physics;
private Weapon magic;
private int level;
private int hurt;
public Hero(){
}
public Hero(String name){
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getBlood() {
return blood;
}
public void setBlood(int blood) {
this.blood = blood;
}
public Weapon getPhysics() {
return physics;
}
public void setPhysics(Weapon physics) {
this.physics = physics;
}
public Weapon getMagic() {
return magic;
}
public void setMagic(Weapon magic) {
this.magic = magic;
}
public int getLevel() {
return level;
}
public void setLevel(int level) {
this.level = level;
}
public int getHurt() {
return hurt;
}
public void setHurt(int hurt) {
this.hurt = hurt;
}
public int remainBlood(int blood,int hurt){
blood-=hurt;
return blood;
}
public void leval(int level){
level++;
System.out.println(level);
}
}
2.3 常量
实例变量可以很好的支持面向对象的封装,但是有些时候我们还是需要在多个对象里共享一些数据,这个时候就需要引入常量了,常量也可以叫做静态变量。
我们看一个例子:
/**
* 学校服务
*/
public class SchoolService{
// 学校数据的常量
public static ArrayList<School> schools = new ArrayList<>();
}
我们定义了静态变量以后,我们可以在主类里面操作他了
public class SchoolServiceTest{
public static void main(String[] args){
School school = new School();
school.setId("1");
school.setName("北京大学");
// 存储数据
SchoolService.schools.add(school);
}
}
在 Java 当中实际上是创建了一个全局唯一的内存空间并且分配给了这个变量。所以常量的值只会随 Java 的销毁才会被销毁,这样也就方便我们存储一些数据到内存中,否则会随着对象的销毁被销毁掉的。
我们一般会在两种情况下面使用常量:
常量的字符串值和需要在内存里面做缓存的值
常量字符串值:我们使用final关键字来修饰,final 代表着这个变量不可以被再次修改用来保证代码安全。在字符串常量场景中经常组合使用。
public static final String name = "Mike"
如果我们需要修改相关name,只需要修改这一处就可以了
内存和缓存:
有些时候为了更高的性能,我们会缓存一些数据到内存里,举个例子我们缓存学校的数据。
静态代码块:
补充介绍一下,static 关键字除了声明静态变量外,还可以声明一个静态代码块(可以在其中执行代码并且调用 static 变量)
package com.youkeda.service;
import com.youkeda.model.School;
/**
* 学校服务
*/
public class SchoolService{
// 学校数据的常量
public static ArrayList<School> SCHOOLS = new ArrayList<>();
// 声明一个静态代码块,用于初始化学校数据
static{
School school = new School();
school.setId("1");
school.setName("北京大学");
SCHOOLS.add(school);
school = new School();
school.setId("2");
school.setName("清华大学");
SCHOOLS.add(school);
}
public static void main(String[] args){
// 测试打印一下内存的数据
for(int i=0;i<SCHOOLS.size();i++){
School school = SCHOOLS.get(i);
System.out.println(school.getName());
}
}
}
2.4 继承
当我们遇到这样具备相同属性以及从属某一类别、领域的对象的时候就可以考虑使用一个新的知识:继承来解决了,这也是面向对象的一项重要的能力。
我们在Java里面采用extends关键字来表示继承。
public class Yasuo extends Hero {
}
这样亚索是子类,Hero是父类
在UML图里面是这么做的,只是例子昂:
如上图这个箭头的图形就代表着继承的关系,注意箭头指向的类是父类哦
补充说明一下,在继承关系中,private 级别的变量或者方法子类是获取不到的,子类如果想获取父类的变量和方法可以设置父类的变量和方法为 protected 或者 public 访问控制符。直接写就报错
2.5 方法覆盖
我们在父方法里面由于方法各个执行情况不一样,我们这个时候就要覆盖重写。
大家发现会有这样的代码:
Hero hero = new Yasuo();
这个叫做多态的使用,这个相当于实现Hero对象
首先hero里面就有attack方法
public class Yasuo extends Hero {
public void attack(){
System.out.println("attack!");
}
}
//在主方法里面调用attack方法,是亚索攻击
Hero hero = new Yasuo();
hero.attack();
虽然我们实例化的是亚索,我们调用可以像父类一样去执行他,我们调用父类attack方法,由于覆盖重写了,就调用亚索attack方法
但是子类可以转化成父类,父类不能转化成子类。
不能Yasuo yasuo = new Hero();这个是不允许的。
2.6 super关键字
当我们掌握了继承这个知识后,很自然的就遇到了一个新的问题,那就是子类如果就是想调用父类的方法怎么操作呢?
这个同样涉及到了类作用域的概念,之前我们学习过this关键字,这个关键字可以操作实例内的变量或者方法。那么操作父类同样也有一个关键字:super,super关键字只能发生在子类里面。
我们看一个例子:
package com.java.LOL;
public class Hero {
protected String name;
private int blood;
private Weapon physics;
private Weapon magic;
private int level;
private int hurt;
public Hero(){
}
public Hero(String name){
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getBlood() {
return blood;
}
public void setBlood(int blood) {
this.blood = blood;
}
public Weapon getPhysics() {
return physics;
}
public void setPhysics(Weapon physics) {
this.physics = physics;
}
public Weapon getMagic() {
return magic;
}
public void setMagic(Weapon magic) {
this.magic = magic;
}
public int getLevel() {
return level;
}
public void setLevel(int level) {
this.level = level;
}
public int getHurt() {
return hurt;
}
public void setHurt(int hurt) {
this.hurt = hurt;
}
public int remainBlood(int blood,int hurt){
blood-=hurt;
return blood;
}
public void leval(int level){
level++;
System.out.println(level);
}
public void attack(){
System.out.println("发起进攻");
}
}
package com.java.LOL;
public class Yasuo extends Hero {
public void attack(){
super.attack();
System.out.println("attack!");
}
}
package com.java.LOL;
public class test {
public static void main(String[] args) {
Hero hero1 = new Hero("亚索");
Hero hero2 = new Hero(null);
Hero hero3 = new Hero(null);
hero1.leval(100);
hero2.remainBlood(250,70);
Hero hero = new Yasuo();
hero.attack();
}
}
我们输出了101 发起进攻 attack!
子类运用 super 调用父类一般会发生在父类有一些通用的逻辑可以被执行。换句话说就是调用时先调用了父类里面相同的方法,再执行子类的。super还可以执行父类不一样的方法可以把attack变成level也是可以的
子类构造函数:
直接super(名称即可)
public Yasuo(String name){
super(name);
}
如果父类定义了构造函数子类不定义就会报错
注意super必须在第一行,否则就会发生报错,和没有定义构造函数一模一样。
2.7 抽象类
由于我们在上述方法里面attack里面有可能没有任何方法,我们可以把attack定为抽象类,使用abstract关键字来实现,这个叫做抽象方法。
class前面加一个abstract关键字
public abstract class Hero{
public abstract void attack();
}
你会发现方法没有大括号,只是以分号结尾。
我们可以用一个类实例化它,但是,里面的抽象方法必须覆盖重写,否则直接报错。
public class Yasuo extends Hero{
public void attack(){
.........
}
}
实例化的方法不能有abstract关键字。
补充说明,抽象类也可以添加普通的方法从而方便子类去复用方法,声明抽象类后,它的子类必须实现抽象方法,如同实现接口一样,接口我们下面会说。
3. 接口
3.1 接口定义
关于抽象类在以前运用的很多,但是随着 Java 对接口的升级,抽象类的使用就变得更低了,我们推荐使用接口,这也是本节课的核心内容:接口。
实际上接口相当于一个抽象类
那就是接口定义了标准、规范,就像各个厂商根据标准生产出自己的货品来,只要接口一致可以无缝的工作起来。在 Java 当中也存在这样的能力,同样也叫接口。关键字是interface。
在 Java 当中,接口也是一个类、也包含方法,我们定义了一个接口类以后,就可以支持多个实现类,接口的调用方不用关心是谁写的实现类,只关心接口的方法参数、方法返回值就可以无缝的完成程序的对接。
接口有很多优点:
可以减少对接损耗
1.接口是规范、标准,是大家都知道这个是**做什么*的,但是不用知道具体怎么做*,就像你知道麦当劳买汉堡,你会吃就行啦,不需要知道汉堡怎么做出来。
2.接口可以反复调用,拥有良好的扩展性。就像门口开了一家新的麦当劳,你就大致知道他卖啥,如果需要详细知道,才进去看一下就行。
3.接口可以减少代码的耦合,降低沟通成本。
可以看看这个文档:
https://www.youkeda.com/post/detail/fa9d7705e0a74ea5ab0dccd42468ce27
定义方法:
UML图:
定义方法与抽象类似
3.2 接口实现
我们使用一个类来连接就可以了,使用implements关键字来
接口定义的方法在实现类里必须要全部实现了,而且方法签名要一模一样(同样的方法名称、方法参数、方法返回值)
接口定义方法是public的,实现方法修饰符也必须是public
接口和继承不同,继承表达的是父子关系,所以子类只能继承一个父类;接口是一个约定,实现类可以拥有多个约定,所以实现类可以实现多个接口,实现多个接口用","分开即可
举个例子:
package com.youkeda.service;
public interface EchoService {
// 输出任意的内容
void echo();
}
package com.youkeda.service.impl;
import com.youkeda.service.RoleService;
import com.youkeda.service.EchoService;
public class RoleServiceImpl
implements RoleService,EchoService{
public void addRole(Role role){
}
public ArrayList<Role> getRoles(){
return null;
}
public void echo(){
}
}
但是接口是不能被实例化的,因为接口只是一个定义,没有具体方法,所以不能被实例化
所以:
EchoService echo = new EchoService();
是错误的,什么是正确的呢?
正确的做法应该是结合实现类的。
package com.youkeda.test;
import com.youkeda.service.RoleService;
import com.youkeda.service.impl.RoleServiceImpl;
/**
* RoleServiceTest
*/
public class RoleServiceTest {
public static void main(String[] args) {
RoleService roleService = new RoleServiceImpl();
}
}
由于 RoleServiceImpl 类同时实现了 EchoService,所以实例也可以被转化为 EchoService。
package com.youkeda.test;
import com.youkeda.service.RoleService;
import com.youkeda.service.impl.RoleServiceImpl;
/**
* RoleServiceTest
*/
public class RoleServiceTest {
public static void main(String[] args) {
RoleService roleService = new RoleServiceImpl();
// 类型转化:把 roleService 实例转化为 EchoService 接口类型
EchoService echoService = (EchoService)roleService;
}
}
UML图如下:
3.3 接口里面的默认方法
我们知道接口定义的方法,实现类必须要全部实现,当我们想复用一些逻辑的时候就比较麻烦了。而且有的时候我们可能只是想自定义实现某个方法而已。
自从 Java8推出接口的默认方法后,抽象类的作用和接口就越来越重合了,所以我们多数情况下还是建议用接口替代抽象类,现在我们来看一下接口的默认方法如何实现的。
举例:
public interface Vehicle {
default void print(){
System.out.println("我是一辆车!");
}
}
接口方法我们使用了default关键字,这个时候实现类如果没有自己的实现逻辑,就可以不用覆盖默认方法。
覆盖默认接口方法实现:
public class Car implements Vehicle {
public void print(){
System.out.println("我是一辆汽车!");
}
}
不实现方法:
public class Car implements Vehicle {
}
二者语法都正确。
public class CarTester {
public static void main(String args[]){
Vehicle vehicle = new Car();
vehicle.print();
}
}
相当于多态。
4. 异常
4.1 异常机制
虽然我们总是很自信,相信自己的代码没有任何问题,但是呢。。。真实的世界是:bug很多。
写出的代码完全没有错误的概率是非常非常低的,所以遇到错误也不用崩了,正常现象。
以下代码没有问题
public class ExceptionTest {
public static void main(String[] args){
int len = getStrLength("字符串");
System.out.println(len);
}
// 获取字符串的长度
public static int getStrLength(String str){
return str.length();
}
}
但是把内容改成null,会抛出异常java.lang.NullPointerException,并且System.out.println(len);方法没有执行了,这就是 Java 中的程序错误了。当出现错误的时候后面的代码就不会继续执行了。
错误包括编译错误(语法不对,可以避免),运行错误
因为代码必须运行后,才会出现。我们会把这种运行时出现的错误称为异常,大多数编程语言都会有异常的概念,所以大家请记住这一点。
因为是运行时出现的错误,所以异常也是可以被我们处理的,这个处理行为就叫捕获
像C语言就是没有异常机制的,这就会导致一旦出错,系统就崩溃。而 Java 推出异常机制是为了让错误不影响整体的系统可用性,也就是说出现一点问题,其他地方还是可以继续工作的。
同时在 Java 中异常处理非常灵活,异常流程和正常流程是分离的。一旦某段程序抛出了异常,这段代码如果有能力处理,就捕获它,否则只需抛出异常由调用者来处理。
java.lang.NullPointerException是我们以后经常看见的异常
null 不是对象,表示空、什么也没有、什么也不是
当异常被捕获后,后面的代码可以继续执行了。这就是异常机制的能力,让程序更健全。
4.2 Java异常类
你可以看出异常类也是 Java 的对象,所以对象该有的特性它也都具备。所有的异常和错误都是继承于 Throwable 类。它的子类有 Error 和 Exception。我们在大多数情况下遇到的是Exception
补充说明一点 java.lang 包是不需要 import 的
Exception 有一个特殊的子类 RuntimeException,这个待会再说。我们先看其他的Exception,上节课我们遇到的 NullPointerException 就是 Exception 的子类,一般情况下我们遇到的异常类都是 Exception 的子类
常见的有IOException、ArithmeticException(数字异常)、NullPointerException(空指针异常)、ArrayIndexOutOfBoundsException(数组下标越界异常)
ClassCastException(类型转化失败异常)
IllegalArgumentException(检查参数是否合法)
具体使用情况:
public class ExceptionTest2 {
public static void main(String[] args) {
try {
int len = getStrLength(null);
} catch (NullPointerException e) {
}
System.out.println("程序执行到这"+len);
}
// 获取字符串的长度
public static int getStrLength(String str) {
return str.length();
}
}
你会程序执行失败了,因为len在try里面,所以如果想要操作 len 这个变量,那么就需要我们提升作用域。就是在 try 之前先定义变量,然后在操作变量即可。
public class ExceptionTest2 {
public static void main(String[] args) {
int len = 0;
try {
len = getStrLength(null);
} catch (NullPointerException e) {
}
System.out.println("程序执行到这"+len);
}
// 获取字符串的长度
public static int getStrLength(String str) {
return str.length();
}
}
多异常捕获,用多个catch代码块进行捕获
public class ExceptionTest4 {
public static void main(String[] args) {
int num = 0;
try {
num = getInt("小王");
} catch (NumberFormatException e) {
} catch (NullPointerException e) {
}
System.out.println(num);
}
// 字符串转换为数字
public static int getInt(String str) {
return Integer.parseInt(str);
}
}
由于很多异常都是继承了Exception类,所以可以直接一个Exception就可以捕捉了,因为有些时候不知道是什么异常。
public class ExceptionTest4 {
public static void main(String[] args) {
int num = 0;
try {
num = getInt("小王");
} catch (Exception e) {
}
System.out.println(num);
}
// 字符串转换为数字
public static int getInt(String str) {
return Integer.parseInt(str);
}
}
4.3 抛出异常
前面我们一种强调异常的捕获,但是异常还是会抛出,我们不知道什么时候会抛出
我们也可以手动的抛出异常,比如我们不知道如何处理一些超出预期的数据的时候,我们就可以抛出异常给上层去处理,如果上层也不知道,那么可以继续向上抛出。
public static int getInt(String str){
if(str == null){
throw new IllegalArgumentException("姓名不能为空");
}
return Integer.parseInt(str);
}
如你所见,只要在你想要抛出的地方使用 throw 关键字就可以抛出异常了,throw 后是具体的异常实例。
我们会看到异常被抛出了
异常的错误显示一般我们称为异常堆栈,从堆栈中是可以看出错误在哪一行,哪个方法调用的,所以优秀的工程师都会很仔细的看异常堆栈的,因为从堆栈里可以看到很多有用的信息来。
RuntimeException
RuntimeException 和其余的 Exception 有个不同点,那就是在我们手工抛出的时候,并不需要在方法上约定可能抛出的异常。现在我们改动一下,如果我们把 IllegalArgumentException 换成 Exception会发生错误
会发现未报告的异常错误java.lang.Exception; 必须对其进行捕获或声明以便抛出
我们就要在方法前面抛出异常
public class ExceptionTest7 {
public static void main(String[] args) {
int num = getInt(null);
System.out.println(num);
}
public static int getInt(String str) throws Exception{
if(str == null){
throw new Exception("姓名不能为空");
}
return Integer.parseInt(str);
}
}
我们还会发现错误,因为调用者没有捕获它
public class ExceptionTest8 {
public static void main(String[] args) {
int num = 0;
try {
num = getInt(null);
} catch (Exception e) {
System.out.println(e.getMessage());
}
System.out.println(num);
}
public static int getInt(String str) throws Exception {
if (str == null) {
throw new Exception("姓名不能为空");
}
return Integer.parseInt(str);
}
}
这次程序就没有发生异常了
为了获取异常的信息,我们还使用了e.getMessage()的方法,这个可以获取异常的字符串消息,这个是在呢哇 Exception(“姓名不可能为空”)的情况下调用的
考虑到代码的安全性,我们建议还是不要用 RuntimeException 异常,因为这个在编译阶段是不做检查的,这样可能会导致一些不必要的问题产生你还不知道
由于我们会输入异常,所以要提取抛出一下。
package com.youkeda.service;
public interface UserService {
User login(String userName,String password) throws NullPointerException;
}
如果接口类抛出了异常,实现类就要捕获它。
4.4 自定义异常
我们首先要创建异常类
public class GirlFriendNotFoundException extends Exception{
}
这个异常类是我自己创建的,定义时候就
public void Couple()throws GirlFriendNotFoundException{}
我们为了防止报错要在类里面抛一下:
举个例子:
public User login(String userName,
String password) throws ParamNullException {
if (userName == null || password == null) {
throw new ParamNullException();
}
for (User user : USERS) {
if (user.getUserName().equals(userName) && user.getPassword().equals(password)) {
return user;
}
}
return null;
}
构造函数:(想要知道哪个异常了)
public User login(String userName,
String password) throws ParamNullException {
if (userName == null) {
throw new ParamNullException("用户名为空");
}
if (password == null) {
throw new ParamNullException("密码为空");
}
for (User user : USERS) {
if (user.getUserName().equals(userName) && user.getPassword().equals(password)) {
return user;
}
}
return null;
}
结果发现报错了,我们调用时在异常里面输入了东西,
看看Exception类以后
我们创建的时候必须要执行 super 构造函数的,所以最后你应该是这样的
public class ParamNullException extends Exception {
public ParamNullException() {
}
public ParamNullException(String msg) {
super(msg);
}
}
如果像刚才空的就会报错
这张图就是一个很好的解释
所以我们自定义异常的时候需要把异常类进行构造函数使用,需要进行信息传递要进行构造,否则报错。
finally方法:
由于我们不知道异常处理以后会干什么,所以我们引入了finally代码块来使用,一般和try进行使用
public void work() throws LeaveEarlyException {
try {
开门
工作 8个小时 // 可能会抛出异常 DiseaseException
关门
} catch (DiseaseException e){
throw new LeaveEarlyException();
} finally{
关门
}
}
finally 在我们以后操作数据库或者其他流的时候会用到,以后我们会写的
5. 内部类
5.1 使用方法
面向对象的核心思想之一就是封装:把所有不希望对外公开的实现细节封装起来。我们在对变量属性的封装之外,还可以在某一个类中定义一个属于它的内部类来封装一些服务。
我们看一个例子:
package com.youkeda;
public class Outer {
public class InnerTool { // 内部类
public int add(int a,int b){
return a+b;
}
}
}
InnerTool 是内部类,Outer是外部类
如果我们想调用内部类,那么就需要先实例化它。
我们实例化只能在Outer类里面实例化
package com.youkeda.util;
public class OuterUtil {
public class InnerTool { // 内部类
public int add(int a,int b){
return a+b;
}
}
private InnerTool tool = new InnerTool();
}
我们看一个代码昂
package com.youkeda.util;
public class OuterUtil {
public class InnerTool { // 内部类
public int add(int a,int b){
return a+b;
}
}
private InnerTool tool = new InnerTool();
public int add(int a,int b,int c){
return tool.add( tool.add(a,b) , c );
}
}
这个不是递归!!!
首先执行a+b,再执行a+b+c
package com.youkeda.test;
import com.youkeda.util.OuterUtil;
public class OuterUtilTest {
public static void main(String[] args){
OuterUtil outer = new OuterUtil();
int sum = outer.add(1,2,3);
System.out.println(sum);
}
}
如果在外面想调用里面的呢?
OuterUtil.InnerTool tool = new OuterUtil().new InnerTool();
我们在外部实例化内部很麻烦,因为这个内部类是属于外部类的属性,所以如果想得到它,就必须先把外部类实例化。
一般情况下,我们也不建议在外面直接调用内部类。此时我们引入了静态内部类。
静态内部类
如同静态变量一样,内部类也可以声明为静态内部类,只要增加 static 关键字即可
package com.youkeda.util;
public class OuterUtil {
public static class InnerTool { // 内部类
public int add(int a,int b){
return a+b;
}
}
}
如果我们声明为静态内部类后,那么就可以直接调用这个内部类,不需要先实例化外部类。因为静态方法不能访问非静态的东西。
package com.youkeda.test;
import com.youkeda.util.OuterUtil;
// 静态内部类是需要单独 import 的
import com.youkeda.util.OuterUtil.InnerTool;
public class OuterUtilTest {
public static void main(String[] args){
InnerTool tool = new InnerTool();
int sum = tool.add(1,2);
System.out.println(sum);
}
}
局部内部类:
public class Tester {
public void test(){
class B{
int a;
}
B b = new B();
}
}
这个是在方法内部定义,看看即可。
内部类的使用场景越来越少了。对于初学者来说,内部类更多的时候是调用某些框架的时候会用到,所以我们这个阶段能够看的懂,会调用就可以了
5.2 匿名类
匿名类在实际工作中使用的比较频繁,所以需要我们去掌握它,本质上它就是一个局部内部类,不同的是匿名类只用于类的实例化,而不是声明
举个例子:我们现在有一个接口A
public interface A {
public void method();
}
我们这么使用:
public class ATest {
public static void main(String[] args){
A a = new A(){
public void method(){
System.out.println("执行内部类");
}
};
a.method();
}
}
我们可以这么理解,由于接口不能被new出来,我们需要一个实现类来进行实现,这个方法就可以不用实现类来实现接口,后面有可能会使用lambda表达式(JDK 8 以后会用)
注意这里:
A a = new A(){
public void method(){
System.out.println("执行内部类");
}
};
效果等价于:
public class AImpl implements A{
public void method(){
System.out.println("执行类");
}
}
除了实现接口以外,也可以继承父类
public class Sub {
public void print(){
}
}
public class SubTest {
public static void main(String[] args){
Sub sub = new Sub(){
public void print(){
System.out.println("执行内部类");
}
};
sub.print();
}
}
等价于:
public class SubNode extends Sub {
public void print(){
System.out.println("执行");
}
}
总结:匿名类相当于继承父类时不直接继承什么类,直接覆盖重写即可。
6. 对象的关系
聚合关系:聚合是一种特殊的关联(Association)形式,表示两个对象之间的所属(has-a)关系。所有者对象称为聚合对象,它的类称为聚合类;从属对象称为被聚合对象,它的类称为被聚合类。Employee类从属于Company类。
public class Company {
private List<Employee> employees;
}
public class Employee {
private String name;
}
这个list我们以后会说明的。
组合关系:聚合是一种较弱形式的对象包含(一个对象包含另一个对象)关系,较强形式是组合(Composition)。在组合关系中包含对象负责被包含对象的创建以及生命周期,即当包含对象被销毁时被包含对象也会不复存在。
public class Car {
// 实例化一个不可变的属性 engine
private final Engine engine = new Engine();
}
public class Engine {
private String type;
}
依赖关系:依赖(Dependency)描述的是一个类的引用用作另一个类的方法的参数。
例如, UserService 和 User 之间就是依赖关系,大家注意看一下线条,依赖和关联的区别在于有没有属性存在,依赖的类是有属性变量的
继承实现,前面好好的说了,这里就不一一解释了。