面向对象
面向对象模式分为:
- OO(Object Oriented)
- OOA : 面向对象的分析
- OOD: 面向对象的设计
- OOAD:面向对象分析与设计,开发效率要高。
- OOP: 面向对象编程(重点)
穿插项目(深海杀手) -------- 分析,设计 , 编程
面向对象决定了入职的速度和薪资。
高质量的代码 == 高薪。
高质量代码:
复用性要好!扩展性要好!维护性要好!可移植性!
可读性要好! 健壮性要好! 效率性要好!
学完面向对象
听不懂我讲的过程,分析,设计等 -------------落课
晕晕乎乎,能理解啥意思. ----------------------正常
听懂了,但是让自己分析或设计不出来 ----优秀
面向对象三大特征**😗*
类和对象:
类:抽象的不具体的事物,通常表示的是一类事物,在程序表示的模板。
对象:具体,真实存在可以看到见或者是摸得到的!
如:
狗是什么颜色的(蓝色 绿色 紫色 红色 黄色 白色)
这种是不能确定的(类)
如果我说这个黄色的柯基是什么颜色的
这就能明确的知道这是一只黄色的狗(对象)
分析项目中有哪些对象?
一个战舰,一堆深水炸弹,一堆侦察潜艇(银色),一堆水雷潜艇(军绿),一堆鱼雷潜艇(金黄),一堆鱼雷(长条) 一堆水雷(圆形)
(ObserverSubmarine)侦察潜艇
举例: 数据
OS1 ---------------------- X , Y , WIDTH , HEIGHT , SPEED
OS2 ---------------------- X , Y , WIDTH , HEIGHT , SPEED
OS3 ---------------------- X , Y , WIDTH , HEIGHT , SPEED
OS4 ---------------------- X , Y , WIDTH , HEIGHT , SPEED
OS5 ---------------------- X , Y , WIDTH , HEIGHT , SPEED
…
OS100 ------------------ X , Y , WIDTH , HEIGHT , SPEED
假设100个侦察潜艇对象,那么对应就有100份同样的数据重复!(代码冗余)
使用类来解决!
类(模具) 对象
假设我今天晚上要做一千个月饼,如果一个一个做(做不完)但是过程是重复的。
使用月饼模具 批量产生月饼
封装
类的封装指的是:将一类别下所有对象共有的属性和行为,封装到一个模板类中的过程。
1.如何创建类(封装):
类中放该类别下所有对象共有的属性/数据 --------------------变量
类中放该类别下所有对象共有的行为/方法 --------------------方法
package day06;
public class Person {
//共有属性
String name;
int age;
char sex;
//共有行为
void eat() {
} //吃
void slep() {
} //睡
void play() {
} //玩
}
class car{ // 汽车类
// 共有属性
String color;
String type;
double price;
// 共有行为
void run(){} // 跑
void back(){} //退后
void stop(){} //停止
}
class ObserverSubmarine{ //侦察潜艇类
//共有的属性
int x;
int y;
int width;
int height;
int speed;
//共有的行为
void step(){ //移动
}
}
变量
成员(全局)变量:声明在类的里面,方法外的变量,称之为成员变量,作用域在整个类中!
局部变量:在方法中声明的变量,称之为局部变量,作用域只在当前方法中!
当前自定义的类也是一种数据类型!引用类型!
package oo.day01;
/**
* x学生模版类:
* 共有属性和行为
* 注意:只有测试类,才会家main,模版类中无需加main
* */
public class Student {
// 共有属性 全局变量:整个类结构都是可以使用
String name; //名称
int age; //年龄
int stuID; //学号
/**
* 为什么在类中声明的变量能再方法中使用呢?
* 我们声明的变量会找当前最近的花括号也就是student { 这个,
* 那么他的作用域就在这个花括号开始到这个花括号结束的位置(也就是在整个类中)。
* 也就是全局变量。
* */
//共有行为
void study() { //学习
int a = 10; // a就是局部变量,只能在这个方法中使用,其它方法使用不了
System.out.println(name + "在刻苦学习");
}
void sayHi() { //打招呼
System.out.println("大家好,我叫:" + name + "今年" + age + "岁, 我的学号是:" + stuID);
}
}
封装:
类的封装指的是:将一类别下所有对象共有的属性和行为,封装到一个莫板块中的过程。
上面模板类有了,那么我们要如何去创建对象呢。
2.如何创建对象
语法:
创建对象语法: 数据类型 变量名 = new 引用数据类型();
// 创建对象语法: 数据类型 变量名 = new 引用数据类型();
Student zs = new Student(); //创建一个学生对象,将对象存放到了 zs这个应用类型变量里面。
Student zs = new Student();//创建一个学生对象 将对象存放到了 zs这个引用类型变量里面
Student ls = new Student();//创建一个学生对象 将对象存放到了 ls这个引用类型变量里面
Student ww = new Student();//创建一个学生对象 将对象存放到了 ww这个引用类型变量里面
3.如何访问对象
package oo.day01;
/**
* 测试类: 用户测试学生类创建对象等相关操作的类
* */
public class StudentDemo {
public static void main(String[] args) {
// 创建对象语法: 数据类型 变量名 = new 引用数据类型();
Student zs = new Student(); //创建一个学生对象,将对象存放到了 zs这个应用类型变量里面。
//zs.name就是调用
// zs 这个变量 能用点调用什么 取决于 当前这个变量的类型中有什么
// 通过那个对象 打点调用属性或者是方法 那么使用的数据或者操作 都是那个对象的
System.out.println(zs.name); // null
System.out.println(zs.age); // 0
System.out.println(zs.stuID); // 0
//赋值
zs.name = "张三";
zs.age = 15;
zs.stuID = 10001;
System.out.println(zs.name); // 张三
System.out.println(zs.age); // 15
System.out.println(zs.stuID); // 10001
//调用方法
zs.sayHi(); // 调用zs这个对象的打招呼行为
zs.study(); // 调用zs这个对象学习的行为
}
}
项目需求:
深海杀手的项目分析:
(1)识别对象
战舰,深水炸弹,侦察潜艇(银色的),鱼雷潜艇(金黄色),水雷潜艇(军绿色),水雷,鱼雷
(2)分配职责
战舰(玩家):发射深水炸弹
深水炸弹 : 攻击潜艇,若打到时,
1.深水炸弹打到潜艇时,深水炸弹和潜艇消失。
2.打到侦察潜艇加10分,打到鱼雷潜艇加40分 —加分的行为
3.打到水雷潜艇加1命 —加命的行为
鱼雷潜艇 :可以发射鱼雷,攻击战舰,若到时,
1.鱼雷要消失
2.战舰减一条命(若命数为0,游戏结束)
水雷潜艇 :可以发射水雷,攻击战舰,若到时,
1.水雷要消失
2.战舰减一条命(若命数为0,游戏结束)
(3)建立交互
- 在src创建一个包 ,包名为 cn.tedu.submainre ,这是我们的项目包,包含项目的文件
因为存在7个* 对象,那么需要对应7个模板类。 对象通过模板来创建
战舰类: ------------------------------------------ Battleship
属性:int x , y , width,height,speed
行为:void step(){ } 移动
深水炸弹类: -------------------------------------- Bomb
属性:int x , y , width,height,speed
行为:void step(){ } 移动
侦察潜艇类: -------------------------------------- ObserverSubmarine
属性:int x , y , width,height,speed
行为:void step(){ } 移动
水雷潜艇类: -------------------------------------- MineSubmarine
属性:int x , y , width,height,speed
行为:void step(){ } 移动
鱼雷潜艇类: -------------------------------------- TorpedoSubmarine
属性:int x , y , width,height,speed
行为:void step(){ } 移动
水雷类: --------------------------------------- Mine
属性:int x , y , width,height,speed
行为:void step(){ } 移动
鱼雷类: --------------------------------------- Torpedo
属性:int x , y , width,height,speed
行为:void step(){ } 移动
项目结构
创建了GameWorld类:
package cn.tedu.submainre;
/**
* 游戏 窗口类: 负责加载 并运行游戏
* 存放的就是运行时对象交互的逻辑
*/
public class GameWorld {
Battleship ship;//声明了一个ship ----成员变量
Bomb bomb;
ObserverSubmarine os;
MineSubmarine ms;
TorpedoSubmarine ts;
Mine m;
Torpedo t;
void action() { //将声明的变量对应的对象都创建出来
ship = new Battleship(); //创建一个战舰对象 并将对象赋值给ship
bomb = new Bomb();
os = new ObserverSubmarine();
ms = new MineSubmarine();
ts = new TorpedoSubmarine();
m = new Mine();
t = new Torpedo();
}
public static void main(String[] args) {
GameWorld gameWorld = new GameWorld();
gameWorld.action();
}
/**
* 1. 为什么要将各类型变量的声明写到main的外面?
* 在main方法中声明的变量为局部变量,但是在后期当前类中还有很多方法需要用到这些类型的变量
* 所以应该设计为成员变量,作用域在整个类中。
*
* 2. 为什么要单独写一个action放大做测试?
* 因为main方法比较特殊,使用了static修饰的方法,那么普通成员(成员变量, 自定义方法)是无法被main直接访问的
* 所以单独做一个普通方法,来进行测试运行。
*
* 3. 为什么要创建GameWorld对象去取调用action方法?
* 应为main方法比较特殊,用static修饰的方法,那么普通成员(成员变量, 自定义的方法)是无法被main直接
* 访问的,我们可以通过创建对象,通过对线.方法的方式进行调用action方法。
* */
}
问题:在GameWorld类的action方法中,创建对象并为对象的每个属性赋值的过程是非常麻烦和重复的!每个对象按照目前来将是需要赋值5个数据。工作量大。
解决:通过构造方法 来解决创建对象赋值的繁琐过程!
构造方法:
构造方法又称之为构造器。
适用性:在创建对象时,可以快速实现为当前对象的属性赋值(初始化赋值)
- 构造方法的语法:类名(){ } (注意:构造方法没返回值那一说)
- 构造方法本质作用:构造方法的本质作用就是创建对象的必要语法。
Java规定:如果不为类添加构造方法,那么系统会赠送一个默认的构造方法。
特性:当类模板被创建对象时,会自动调用类中提供的构造方法。
-
如果为类添加了构造方法,那么系统也不在赠送默认的无参构造方法。
-
构造方法是可以重载的!只要满足参数类型或参数个数不同即可!
在运行类中创建对象的时候,就会调用这个类的构造方法,那么我们就可以直接将赋值的变量放在里面
Java规定:局部变量与成员变量的名字是可以重名的!在使用变量时,遵循就近原则。
this关键字
this指代的是当前对象,哪个对象打点调用的方法,那么那个方法中当时用的成员变量指代的就是那个对象的。
this可以解决,当局部变量与成员变量名字冲突时,我们可以通过使用this关键字来明确表示访问的是成员变量!
package oo.day01;
/**
* x学生模版类:
* 共有属性和行为
* 注意:只有测试类,才会家main,模版类中无需加main
*/
public class Student {
// 共有属性 全局变量
String name; //名称
int age; //年龄
int stuID; //学号
//构造函数在类中一般建议写两个,一个是无参数,一个是有参数的
Student() { //无参数的构造方法
}
Student(String name, int age, int stuID) { //由创建对象时来传递具体的数据
this.name = name;
this.age = age;
this.stuID = stuID;
}
//共有行为
void study() { //学习
int a = 10; // a就是局部变量,只能在这个方法中使用,其它方法使用不了
System.out.println(name + "在刻苦学习");
}
void sayHi() { //打招呼
System.out.println("大家好,我叫:" + name + "今年" + age + "岁, 我的学号是:" + stuID);
}
}
main
{
Student zs = new Student();
Student zs = new Student();
Student zs = new Student();
}
zs.study(); -----------------------this 指代的是 zs 这个对象
ls.study(); ------------------------this 指代的是 ls 这个对象
ww.study(); ------------------------this 指代的是 ww 这个对象
可以尝试 将 潜艇杀手项目中 赋值冗余的过程进行优化处理。
只做战舰类的优化。
引用类型的内存图
默认值:引用类型默认值都是null(String,数组,自定义类)
值类型默认值:整数类型默认值为0,小数类型默认值0.0, boolean类型默认为false
当使用空(null)对象,去打点访问内容时,则会报NullPointerException:空指针异常.
如图:
一般声明引用类型的变量在使用之前**,记得赋值对象.**
内存
内存是JVM来进行分配划分:栈区,堆区,方法区(后续会讲)
-
栈区:用来存放局部变量的区域,局部变量指的是方法中声明的变量.特性:方法执行完以后,方法中声明变量也会被随之销毁.
-
堆区:用来存放对象的区域,对象指的是通过new关键字语法创建的对象!特性:堆区的对象,若没有被引用的话,则会变为内存垃圾.
GC(垃圾回收器): 会不定时的检查并清理堆中存放的垃圾.
引用类型的内存图:
Student s = new Student();当执行到这里之后,在堆中就会存在(内存地址假设是0x1)
0x1这个地址中有name(null) age(0) stuID(0) 。
Student s 这个变量是局部变量,就会分配到栈中 是student类型。
= 是一个赋值号,是将右边创建的地址0x1 赋值给 栈中S变量。
在下方打印的时候s.name 这是s就会通过绑定的地址找到堆中相同地址的内容
赋值:
s.name= ‘小王’ 这里就是通过s(栈)中存储的0x1找到堆中存储这个对象的地址0x1 并将name的值改为小王
数组的内存图**😗*
基本数据类型数组
int[] array = new int[3];
如图:
当创建数组的时候在堆中就会开辟一块空间地址(9x1)其中有3块空间(都是int类型)默认值都是0 并且还有下标
之后赋值到array 地址(9x1)
打印时 array通过栈中的地址找到堆,并找到索引为0的数据
赋值时array通过栈中的地址找到堆,并找到索引为0的位置,并赋值100
引用数据类型数组
第一段是先创建一个student类型的数组地址(0x1001)(并有三块空间类型是student,默认值是null,每块空间都是索引)并指向栈中的students
打印时,栈通过堆指向的地址,找到了堆,然后根据索引找到了具体的空间并获取到具体的内容
赋值时,再先在堆中重新创建一个对象(0x1),之后再将这个地址指向栈中的students[0]
,栈通过students[0] 堆指向的地址,找到了堆,然后根据索引找到了具体的空间, 在对这块空间中的某个属性赋值。
//应用类型数组如何使用
Student[] stu1 = new Student[3];
stu1[0] = new Student();
stu1[0].name = "rencai";
stu1[0].age = 15;
stu1[0].stuID = 10001;
System.out.println(stu1[0].name);
System.out.println(stu1[0].age);
System.out.println(stu1[0].stuID);
项目问题:当项目7个类,若被创建对象时,需要依次打点调用对象并赋值,过程非常麻烦。
解决,为当前7个模板类添加构造方法:
Battleship() { //战舰类构造方法
x = 270;
y = 124;
width = 66;
height = 26;
speed = 20;
}
Bomb(int x, int y) { //深水炸弹类构造方法
this.x = x;
this.y = y;
width = 9;
height = 12;
speed = 3;
}
Mine(int x, int y) { // 水雷类的构造方法
this.x = x;
this.y = y;
width = 11;
height = 11;
speed = 2;
}
Torpedo(int x, int y) { //鱼雷类的构造方法
this.x = x;
this.y = y;
width = 5;
height = 18;
speed = 2;
}
这里水雷潜艇 侦察潜艇 鱼雷潜艇这些一看是不应该是直接出现在屏幕中的,索引潜艇的X 应该是为负的这样一看是就不能再屏幕中看到了
Y 值, 潜艇的Y值都是随机的, 范围是150-479
MineSubmarine() { //水雷潜艇类的构造方法
width = 63;
height = 19;
x = -width;
y = (int) (Math.random() * (479 - height - 150) + 150);
speed = (int) (Math.random() * (3 - 1) + 1);
}
ObserverSubmarine() { //侦察潜艇的构造方法
width = 63;
height = 19;
x = -width;
y = (int) (Math.random() * (479 - height - 150) + 150);
speed = (int) (Math.random() * (3 - 1) + 1);
}
TorpedoSubmarine() { //鱼雷潜艇的构造方法
width = 64;
height = 20;
x = -width;
y = (int)(Math.random()*(479-height-150)+150);
speed = (int)(Math.random()*(3 - 1)+1);
}
注意:这里具体的宽高,取决于图片的大小
问题:除了战舰只有一个,其它类型都会存在多个对象,如何存储同一类型的多个对象。
解决:可以用数组来表示。
Battleship ship;//声明了一个ship ----成员变量
Bomb[] bombs;//声明一个深水炸弹数组的变量
ObserverSubmarine[] os;//声明一个侦察潜艇数组的变量
MineSubmarine[] ms;//声明一个水雷潜艇数组的变量
TorpedoSubmarine[] ts;//声明一个鱼雷潜艇数组的变量
Mine[] m;//声明一个水雷数组的变量
Torpedo[] t;//声明一个鱼雷数组的变量
问题:7个模板类,存在共性属性和行为,代码冗余
解决:可以通过继承来解决.
继承:
生活中的继承: 继承财产,钱不用自己挣,继承过来也能花.
------------------- 继承皇位,江山不用自己打,继承过来也能坐.
软件中的继承:继承代码,代码不用自己写,继承过来也能用.
继承的适用性:当多个类之间存在一些共性的属性和行为时,且它们在概念上达到(is a)是一种的关系,才可以使用继承来优化代码.
-
父类/超类:存放所有子类共有的属性和行为.
-
子类/派生类: 存放自己特有的属性和行为.
-
继承的语法: 通过extends****来实现继承 子类名 extends 父类名
一旦实现了继承关系,那么子类就拥有父类中的所有代码,继承过来代表自己有.
-
子类对象,不仅仅可以访问自己的内容,也可以访问父类中的内容.
-
父类对象 只能访问自己的内容.
继承的特性:具有单一性(一个类只能继承一个父类),具有传递性
泛化: 从多个类中提取冗余重复的代码到父类的过程,称之为泛化.
class Person{ //人类 -----父类(超类)
//属性
String name;
int age;
char sex;
//行为
void sayHi(){
}
}
class Student extends Person {//学生类模板 ----子类(派生类)
// 这里Student 继承Person这里类 继承者拥有父类的所有属性和方法
int stuID;
//行为
void study(){
}
}
class Teacher extends Person {//老师类模板
//属性
double salary;
//行为
void teach(){
}
}
class Doctor extends Person{//医生类模板
//属性
int level;
//行为
void cut(){
}
}
package oo.day02;
/**
* 测试(执行)类
*/
public class ExtendsDemo {
public static void main(String[] args) {
Student s = new Student();
//子类对象,不仅可以访问自己的内容,也可以访问父类的内容
s.name = "XX";
s.age = 19;
s.sex = '男';
s.stuID = 1001;
s.sayHi();
s.study();
Person p = new Person();
p.name = "p1";
p.age = 21;
p.sex = '女';
// p.stuID= ?; 父类型对象 无法访问 子类的成员(属性或方法)
// p.salary = ?;
// p.level = ?;
}
}
继承传递性:
class 爷爷类{
传家宝();
}
class 儿子类 extends 爷爷类{
传家宝();
}
class 孙子类 extends 儿子类{
传家宝();
}
传递性也就是孙子类继承儿子类,儿子类继承爷爷类,爷爷类中的内容孙子类也是可以访问的。
解决**😗*
将7个类模板中共有的属性和行为,提取到父类SeaObject中,然后7个类通过继承来实现代码复用.
package cn.tedu.submainre;
/**
* 海洋对象类:
* 当前7个模版类的父类,存放这些类共有的属性和行为
* */
public class SeaObject {
//子类共有属性
int x;
int y;
int width;
int height;
int speed;
// 子类共有行为
void step() {}
}
Java规定:
规定1
实现子类之前,父类默认有自己的构造方法**,构造方法不可以继承的.**
如果实现了继承关系,在创建子类对象时,子类构造方法中会先去调用父类的构造方法,然后再执行子类构造方法,若没有明确为父类或者子类提供构造方法,那么都会有一个默认的构造方法. 子类的构造方法 有个默认隐式写法: super();
现象:创建子类对象时,一定会先执行父类的构造方法,再执行子类自己的构造方法.
package oo.day02;
/**
* 父与子构造方法的使用演示类
* */
public class SuperDemo {
public static void main(String[] args) {
Boo b = new Boo();
}
}
class Aoo {
Aoo() {
System.out.println("Aoo类的构造方法");
}
}
class Boo extends Aoo {
Boo () {
super(); // 调用父类无参构造方法(写不写都有,写必须写在子类构造方法的第一行。)
System.out.println("Boo类的构造方法");
}
}
总结:
在创建子类对象时,会先执行父类的构造方法,之后再去执行子类构造方法。
规定2
若父类写了有参构造方法,而没有提供无参构造方法时,子类构造方法默认还是在调用父类的构造方法,那么则会报错.
解决:需要手动选择父类有参构造方法.
package oo.day02;
/**
* 父与子构造方法的使用演示类
* */
public class SuperDemo {
public static void main(String[] args) {
Boo b = new Boo();
}
}
class Aoo {
Aoo(int a) {
}
Aoo () {}
}
class Boo extends Aoo {
Boo() {
super(10); //若父类写了有参数的构造方法 ,那么子类构造方法中需明确使用父类提供的有参构造方法
}
}
super关键字
super代表是父类.
super.成员变量 ------------------------------访问的是父类的成员变量(应用率低)
super.方法 ----------------------------- -访问的是父类的方法
super(); -------------------------------访问父类的无参构造方法,如果括号写了对应参数,则 表示访问父类有参数构造方法
问题:当前侦查潜艇/鱼雷潜艇/水雷潜艇的构造方法冗余重复.
解决:创建子类对象时,一定会先去执行父类的构造方法,我们可以将冗余重复的构造方法代码提取到父类的构造方法中,达到代码的复用.
/**
* 此构造方法时专门位三种潜艇提供的构造方法
* 因为潜艇的宽高不容,宽高做成形式参数,由具体调用子类来传递具体的内容
* x y speed 初始化数据都是一样的,所以可以写死、
* */
SeaObject(int width, int height) {
this.width = width;
this.height = height;
x = -width;
y = (int)(Math.random()*(479-height-150)+150);
speed = (int)(Math.random()*(3 - 1)+1);
}
三种潜艇的构造方法分别调用父类的构造方法传递具体宽高即可:
剩下4个类,战舰/深水炸弹/水雷/鱼雷 的构造方法 赋值的过程是重复的,可以在父类中提供一个专门为4个类 对象赋值的构造方法.
/**
* 此构造犯法是战舰,深水炸弹 鱼雷 水雷提供的
* 因此这4个类的数据都是不同,所以数据全部做成形式参数
* */
SeaObject(int x, int y, int width, int height, int speed) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.speed = speed;
}
4个类的构造方法:
强化构造方法的练习内容:
package oo.day02;
import java.sql.SQLOutput;
/**
* 人类
*/
public class Person {
String name;
int age;
char sex;
Person(String name, int age, char sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
void sayHi() {
System.out.println("我是" + name + "今年" + age + "岁,性别" +sex);
}
}
package oo.day02;
/**
* 医生类
*/
public class Doctor extends Person {
int level;
Doctor(String name, int age, char sex, int level) {
super(name, age, sex);
this.level = level;
}
void cut() {
}
}
package oo.day02;
/**
* 学生类
*/
public class Student extends Person {
int stuID;
Student(String name, int age, char sex, int stuID) {
super(name, age, sex); // 调用父类的构造方法, 将外部传递进入的数据 提供给父类构造方法完成初始化
this.stuID = stuID;
}
void study() {
}
}
package oo.day02;
/**
* 老师类
*/
public class Teacher extends Person {
double salary;
Teacher(String name, int age, char sex, double salary) {
super(name, age, sex);
this.salary = salary;
}
void teach() {
}
}
测试类如下:
package oo.day02;
/**
* 测试(执行)类
*/
public class ExtendsDemo {
public static void main(String[] args) {
/** 需求:
* 1.若要为学生类 老师类 医生类 创建对象,每次都要依次打点调用并赋值,如何为创建的对象更快速赋值?
* 2.在学生类 老师类 医生类的构造方法中 有没有冗余重复的代码?如果有 如何提取?如何设计?
*/
Doctor d = new Doctor("d1", 18, '男', 1);
Student s = new Student("s1", 19, '女', 1001);
Teacher t = new Teacher("t1", 23, '男', 10000.00);
// d.sayHi();
// s.sayHi();
// t.sayHi();
// System.out.println(s.name);
// System.out.println(t.name);
// System.out.println(d.name);
Student[] students = new Student[3];
students[0] = new Student("s2", 19, '女', 1002);
students[1] = new Student("s3", 20, '男', 1003);
students[2] = new Student("s4", 21, '女', 1004);
// 输入students这个对象的所有sayHi
for (int i = 0; i < students.length ; i++) {
students[i].sayHi(); // 访问数组中每个对象的sayHi方法
}
Doctor[] doctors = new Doctor[3];
doctors[0] = new Doctor("D2", 19, '女', 20);
doctors[1] = new Doctor("D3", 20, '男', 15);
doctors[2] = new Doctor("D4", 21, '女', 10);
for (int i = 0; i < doctors.length ; i++) {
doctors[i].sayHi(); // 访问数组中每个对象的sayHi方法
}
Teacher[] teachers = new Teacher[3];
teachers[0] = new Teacher("t2", 19, '女',10000.00);
teachers[1] = new Teacher("t3", 30, '女',30000.00);
teachers[2] = new Teacher("t4", 25, '女',20000.00);
for (int i = 0; i < teachers.length ; i++) {
teachers[i].sayHi(); // 访问数组中每个对象的sayHi方法
}
// Student s = new Student();
// //子类对象,不仅可以访问自己的内容,也可以访问父类的内容
// s.name = "XX";
// s.age = 19;
// s.sex = '男';
// s.stuID = 1001;
// s.sayHi();
// s.study();
//
// Person p = new Person();
// p.name = "p1";
// p.age = 21;
// p.sex = '女';
// p.stuID= ?; 父类型对象 无法访问 子类的成员(属性或方法)
// p.salary = ?;
// p.level = ?;
}
}
问题:在测试代码中,因为有三个数组,学生数组,老师数组,医生数组,那么去遍历当前数组的每个对象并调用方法,我们需要写3个for循环.能不能将三个循环变成一个循环。
解决:把3个数组 用一个数组来表示即可解决.
向上造型:
1.声明父类型 new 子对象 的语法就叫做向上造型.
**2.**父大 子小
3.向上造型的好处,可以用父类型 来代表不同的子类型.
class Animal{ //动物类
}
class Tiger extends Animal{ //老虎类
}
main{
声明类型 创建的对象
Animal a = new Animal();//动物 是 动物 吗? 语义通
Tiger t = new Tiger(); //老虎 是 老虎 吗? 语义通
Animal a1 = new Tiger();//老虎 是 动物 吗? 语法通
Tiger t1 = new Animal();//动物 是 老虎 吗? 语义不通 -----程序中则会报错!
}
使用向上造型来优化多个for循环遍历的代码:
package oo.day02;
/**
* 测试(执行)类
*/
public class ExtendsDemo {
public static void main(String[] args) {
/** 需求:
* 1.若要为学生类 老师类 医生类 创建对象,每次都要依次打点调用并赋值,如何为创建的对象更快速赋值?
* 2.在学生类 老师类 医生类的构造方法中 有没有冗余重复的代码?如果有 如何提取?如何设计?
*/
// Doctor d = new Doctor("d1", 18, '男', 1);
// Student s = new Student("s1", 19, '女', 1001);
// Teacher t = new Teacher("t1", 23, '男', 10000.00);
//向上造型 :声明父 new 子
Person p1 = new Doctor("d1", 18, '男', 1);
Person p2 = new Student("s1", 19, '女', 1001);
Person p3 = new Teacher("t1", 23, '男', 10000.00);
/**
* 如上创建都是父类型, 一个父类型可以存储不同得分子对象。
* */
// 成功实现一个数组,存储不同的子类对象。
Person[] PP = new Person[9];
PP[0] = new Student("s2", 19, '女', 1002);
PP[1] = new Student("s3", 20, '男', 1003);
PP[2] = new Student("s4", 21, '女', 1004);
PP[3] = new Doctor("D2", 19, '女', 20);
PP[4] = new Doctor("D3", 20, '男', 15);
PP[5] = new Doctor("D4", 21, '女', 10);
PP[6] = new Teacher("t2", 19, '女', 10000.00);
PP[7] = new Teacher("t3", 30, '女', 30000.00);
PP[8] = new Teacher("t4", 25, '女', 20000.00);
for (int i = 0; i < PP.length; i++) {
PP[i].sayHi();
}
}
}
好处使用父类型可以管理不同子类型对象。
问题**😗*
在GameWorld类的action方法中,由于三种潜艇,二种雷 都是分别用对应的类型数组来表示多个对象,目前是5个类型数组,在遍历这些数组中对象时,需要写5次for循环.比较重复冗余.
解决:因为三种潜艇的行为是一样的(移动/被攻击),可以用一个父类型数组来代表这三种潜艇.
因为两种类的行为是一样的(移动/攻击战舰),可以用一个父类型数组来代表这二种雷.
package cn.tedu.submainre;
/**
* 游戏 窗口类: 负责加载 并运行游戏
* 存放的就是运行时对象交互的逻辑
*/
public class GameWorld {
Battleship ship;//声明了一个ship ----成员变量
Bomb[] bombs;//声明一个深水炸弹数组的变量
SeaObject[] submarines;// 代表三种潜艇的父类型数组
SeaObject[] thunders; //代表二种雷的父类型数组
void action() { //将声明的变量对应的对象都创建出来
ship = new Battleship(); //创建一个战舰对象 并将对象赋值给ship
bombs = new Bomb[3];
bombs[0] = new Bomb(10, 10);
bombs[1] = new Bomb(10, 14);
bombs[2] = new Bomb(10, 16);
submarines = new SeaObject[9];
submarines[0] = new ObserverSubmarine();
submarines[1] = new ObserverSubmarine();
submarines[2] = new ObserverSubmarine();
submarines[3] = new MineSubmarine();
submarines[4] = new MineSubmarine();
submarines[5] = new MineSubmarine();
submarines[6] = new TorpedoSubmarine();
submarines[7] = new TorpedoSubmarine();
submarines[8] = new TorpedoSubmarine();
for (int i = 0; i < submarines.length; i++) {
submarines[i].step();
}
// os = new ObserverSubmarine[3];
// os[0] = new ObserverSubmarine();
// os[1] = new ObserverSubmarine();
// os[2] = new ObserverSubmarine();
//
// ms = new MineSubmarine[3];
// ms[0] = new MineSubmarine();
// ms[1] = new MineSubmarine();
// ms[2] = new MineSubmarine();
//
// ts = new TorpedoSubmarine[3];
// ts[0] = new TorpedoSubmarine();
// ts[1] = new TorpedoSubmarine();
// ts[2] = new TorpedoSubmarine();
thunders = new SeaObject[4];
thunders[0] = new Mine(1, 2);
thunders[1] = new Mine(1, 2);
thunders[2] = new Torpedo(1, 2);
thunders[3] = new Torpedo(1, 2);
for (int i = 0; i < thunders.length; i++) {
thunders[i].step();
}
// m = new Mine[3];
// t = new Torpedo[3];
}
}
问题**😗*
运行测试,当前打印输出的信息不明确,例如项目中测试信息,包括继承测试类中的打印信息不明确.
这里打印的方法使用的都是父类的方法,如果要是在父类方法中添加一个子类专有的属性,那么是错误的。
解决: 可以使用重写来解决
重写(Override)
适用性:当父类中的方法子类不受用,我们子类可以通过重写来实现,子类需要执行的逻辑。
重写标准遵循两同两小一大原则:
两同:方法名相同,参数列表要相同。
两小:子类重写父类方法时,若父类有返回值,则子类重写的方法返回值要小于或者等于父 类的返回值类型。
子类重写父类方法时,若父类有异常处理的代码,那么子类的异常处理类型要小于或等 于父类的异常。
一大:子类在重写父类方法时,访问权限要大于或等于父类的方法。
常规重写:基本跟父类方法一模一样 即可实现重写。
现象:在编译期间调用父方法 运行期间执行子方法**(具体看对象)**
学生类重写:
void sayHi() { // 重写的逻辑内容:加上当前类型特有的属性
System.out.println("我是" + name + "今年" + age + "岁,性别" + sex + "学号是:" + stuID);
}
医生类重写:
void sayHi() { // 重写的逻辑内容:加上当前类型特有的属性
System.out.println("我是" + name + "今年" + age + "岁,性别" + sex + "我的职称是:" + level);
}
老师类重写:
void sayHi() { // 重写的逻辑内容:加上当前类型特有的属性
System.out.println("我是" + name + "今年" + age + "岁,性别" + sex + "g工资是:" + salary);
}
}
项目中7个子类重写
战舰类重写:
@Override
void step() {
System.out.println("战舰通过键盘左右移动.....");
}
深水炸弹重写:
@Override
void step() {
System.out.println("深水炸弹y向下运行....");
}
水雷重写:
@Override
void step() {
System.out.println("水雷y向上运动...");
}
水雷潜艇重写:
@Override
void step() {
System.out.println("水雷潜艇x向右运动...");
}
侦察潜艇类重写:
@Override
void step() {
System.out.println("侦查潜艇x向右运动...");
}
鱼雷类重写:
@Override
void step() {
System.out.println("鱼雷y向上运动...");
}
鱼雷潜艇重写:
@Override
void step() {
System.out.println("鱼雷潜艇x向右运动...");
}
重写强化:
重写的情况分类:
情况一:Boo子类只想吃西餐 -----不需要重写
情况二:Boo子类只想喝果汁 -----需要重写
情况三:Boo子类又想吃西餐又想喝果汁 ----需要重写自己逻辑前 通过super调用父类那个方法。
class Aoo{ //父类
void eat(){
System.out.println("吃西餐");
}
}
class Boo extends Aoo{//子类
void eat(){ //----重写
super.eat();
System.out.println("喝果汁");
}
}
面试题:
重写与重载的区别?
重写(Override/Overrideing):发生在父子关系中,方法名相同,参数列表要相同
重载(Overload/Overloading):发生在同一个类中(继承过来也算自己的),方法名相同,参数列表要不同(参数类型不同或参数个数不同)。
class Aoo{ //父类
void show(){
}
}
class Boo extends Aoo{
void show(){ -----发生了重写
}
}
class Aoo{ //父类
void show(){
}
}
class Boo extends Aoo{
void show(int a){ -----发生了重载
}
}
其它
画窗口
界面相关的绘制的代码,这些代码我们不需要了解,以后工作不用,只是当前项目需要绘制窗口而已。
画窗口的步骤:1.做一个画框 2. 找一个底板
在Java中提供,画框的功能和底板的功能。
1.在GameWorld类上方导入两个功能:
import javax.swing.JFrame;//导入画框功能
import javax.swing.JPanel;//导入底板功能
2.我们认为当前GameWorld类,后期要创建游戏中的所有对象,那么也就意味着当前这个类是负责加载并运行项目的类,那么可以将当前类来表示为具体的底板类。
实现继承 Jpanle
public class GameWorld extends JPanel{}
3.实现绘制窗口的逻辑,要放main中调用
package(包)
1.作用:用来区分我们创建的类文件的。避免类名冲突。
2.可以通过包来区分不同模块的业务类。
3.包名要求全小写,包名要按照 : 域名反写.项目名称.模块名称.类名
tedu.cn 域名 之后就是项目名称
全包名:包名.类名
现象:包名只能在类文件的第一行。
package a;
class Aoo{ //全包名: a.Aoo
}
package b;
class Aoo{ //全包名:b.Aoo
}
import(导入)
1.当前类需要使用到某些类,而这些类不在当前类的同包中,那么则需要通过import关键字来导入要使用的类的全包名!
使用java的某些功能时 需要导包。
2.当前类需要使用的类,在当前类的同包下 可以直接使用。
访问修饰符
属性封装:属性私有化,方法公开化,保护程序的合法性,健壮性。
可以让外界访问你想让他访问的内容,隐藏自己的一些内容。
访问修饰符可以修饰 类 、属性、方法
public(公开): 访问权限最大,任意地方都可访问(不同包需要导入)。
private(私有): 访问权限最小,只能在当前类可见。
protected(保护):一般写在父类中的修饰符.当前类/同包类/派生类可见
默认:不加访问修饰符为默认的修饰符,当前类/同包类 可见。
class Card{ //卡类
private int cardID;//卡账户
private int cardPwd;//卡密码
private int balance;//余额
public boolean payMoney(int money){
if( balance >= money ){
balance -= money;
return true;//扣款成功
}else{
return false;//未扣款成功。
}
}
public boolean checkPwd(int pwd){
if(pwd 与 cardPwd 相符合){
//密码正确
return true;
}
else{
return false; //输入不正确
}
}
}
创建test01 包
Aoo与 Boo的演示:
package oo.day03.test01;
/**
* 访问修饰符使用演示类
* public : 任意类
* protected: 当前类 同包类 通过对象都可以访问 派生类(直接使用)
* 默认:当前类 同包类
* private: 当前类
*/
public class Aoo {
public int a;
protected int b;
int c;
private int d;
void show() {
a = 1; // 类内部可见
b = 2; // 类内部可见
c = 3; // 类内部可见
d = 4; // 类内部可见
}
}
class Boo { //private 修饰符的使用 默认的修饰物的使用
void show() {
Aoo aoo = new Aoo();
aoo.a = 1;
aoo.b = 2;
aoo.c = 3;
// aoo.d = 4;无法访问私有的成员
}
}
创建test02包
Coo与 Doo的演示:
package oo.day03.test02;
import oo.day03.test01.Aoo;
/**
* Coo 使用非同包的Aoo 这个类 来访问类中的属性
* */
public class Coo {
void show() {
Aoo aoo = new Aoo();
aoo.a = 1;
// aoo.b = 2; 非同包类不可见
// aoo.c = 3; 非同包类不可见
// aoo.d = 4; 非同包类不可见
}
}
/**
* Doo 是非同包
* */
class Doo extends Aoo{
void show() {
Aoo aoo = new Aoo();
aoo.a = 1;
b = 2; //非同包的派生类 直接 访问即可。
// aoo.c = 3;
// aoo.d = 4;
}
}
访问修饰符总结:
访问修饰符 | 当前类 | 同包类 | 派生类(子类) | 非通报类 |
---|---|---|---|---|
public | √ | √ | √ | √ |
protected | √ | √ | √ | X |
默认 | √ | √ | X | X |
private | √ | X | X | X |
final(最终)
final是一个关键字,可以用来修饰类 、属性 、方法。
修饰属性:用final修饰的属性,不能够二次修改!且修饰成员变量,需声明初始化赋值。
修饰方法:用final修饰的方法,不能够被子类重写!(存在继承情况,应用在父级代码)
修饰类: 用final修饰的类,不能被子类继承!
package oo.day03;
/**
* final的使用演示类:
*/
public class FinalDemo {
private int a;
private final int b = 20; //final声明的属性,申明时必须先初始化。
void show() {
a = 10;
System.out.println(b);
// b = 1002; final //修饰的属性不能二次修改
}
}
class Eoo { //父类
public final void eta() {
}
}
class Foo extends Eoo { //子类
// @Override
// public void eta() { //默认 本类 同包类 不能重写父类中final修饰的方法
//
// }
}
问题:
每一层的学生都是对象,手里拿的杯子,属于对象(个人)。
那当前教学楼层的饮水机属于学生对象吗?不属于对象,但是被对象共享。
static(静态)
静态变量
成员变量分两种:
1.实例变量:属于对象的(有多少个对象就有多少份),在堆中存储。
访问:通过对象打点形式访问。
2.静态变量: 属于类的(且只有一份),在方法区中存储,被所有对象共享。
访问:通过类打点形式访问。
静态变量:用static 关键字修饰的变量 称之为静态变量。
一个类对应的.class字节码文件会在当前类首次被使用时加载,且只加载一次。
JVM:分配 栈区、堆区 、方法区
1、栈区:用来存放局部变量的区域
2、堆区:用来存放对象的区域
3、方法区:加载.class字节码文件(类中的方法/静态变量)
package oo.day04;
/**
* 静态变量使用演示类
*/
public class StaticDemo {
public static void main(String[] args) {
Aoo a1 = new Aoo();
a1.show();
Aoo a2 = new Aoo();
a2.show();
Aoo a3 = new Aoo();
a3.show();
/**
* a的值为:1,b的值为:1
* a的值为:1,b的值为:2
* a的值为:1,b的值为:3
* 可以看到a b的值是不一样的,
* 那么就可以这样去理解:
* 假设还是上面的案例, 杯子,假设是三个人去接水,那么使用的杯子是3个
* 饮水机它的水量只有一份,刚开始水量是满的(1L)
* 第一个人接了0.2L 第二个人接了0.3L 第三个人接了0.1L
* 三个人的杯子里面的水是互不影响的(一个人杯子里面有水,不会影响其它杯子)
* 第一个人访问之后,第二个人访问的是第一个人修改的内容.....
* 所以a得值始终是1, 因为每个a都是一个单独的对象(实例变量)
* 但是b变量不是,b 变量是静态变量,属于类的,被所有对象共享,在第一个对象调用执行了b++
* 第二个对象在进行调用时,一定是基于上一次自增的值 进行自增 ....
* */
}
}
class Aoo {
public int a; //实例变量----> 属于对象(如上面案例的杯子)
public static int b; // 静态变量--->属于类(如上面案例的饮水机)
Aoo() {
a++;
b++;
}
public void show() {
System.out.println("a的值为:" + a + ",b的值为:" + b);
}
}
内存图:
理解:
在首次创建对象时,Aoo会将字节码文件加载到方法区中(有一个静态变量b默认值是0,还有一个构造方法 show方法),然后在堆中创建一个对象,之后就会执行Aoo的无参构造方法 执行a++ b++ 结束,a自增执行的是当前对象(a就变成1 ) b也进行了自增,所以b就变成了1,并将堆中的内存地址指向a1,最后在执行show方法打印a和b
之后又进行创建对象的操作 此时Aoo类不是首次使用,那么就不会再执行加载方法区的操作了,直接进行对象创建的操作了,之后就会执行Aoo的无参构造方法 执行a++ b++ 结束,此时这个a是当前对象的a(有this.)的隐示写法(a就变成1),b++是静态方法被所有对象共享,上一个对象执行之后,b就是1了,那么这里执行完成之后就变成2了,并将堆中的内存地址指向a2,最后在执行show方法打印a和b
静态的适用性:
当有一份数据,需要被某个类别下的所有对象共享时,可以使用静态。
例如:有一份侦察潜艇图片,所有的侦察潜艇对象都需要显示使用这张图片,那么就可以做一份静态的图片,让所有侦察潜艇对象共享这一份图片。
静态方法
用static 关键字修饰的方法称之为静态方法。
1.静态方法属于类的,访问通过类名打点形式访问。
2.存储在方法区中。
3.静态的方法中,没有隐式的this传递.
package oo.day04;
/**
* 静态方法的使用
*/
public class StaticDemo2 {
public static void main(String[] args) {
// 访问show方法 必须先创建对象 使用对象.的方式
Boo boo = new Boo();
boo.show(); // 不能通过类名.方法的形式
// 访问show2方法 这个方法是静态方法,可以直接使用类名.方法的形式调用
Boo.show2(); //直接通过类名的方式访问静态方法
}
}
class Boo {
public int a; //实例变量
public static int b; // 静态变量
public void show() {
this.a = 10; //显示写法 this
Boo.b = 20; //显示写法 类名
}
//静态的方法是不能直接访问实例成员(普通变量 普通方法都是实例成员)
public static void show2() { //隐式的this是无法在静态方法中传递的
// this.a = 11; 无法传递this
Boo.b = 21;
}
}
静态方法适用性:
当做工具的时候,才会用到,用static修饰的方法外部访问可以直接类名点的形式调用,很方便
静态代码块:
1.用static修饰的代码块,称之为静态代码块。
2.静态代码块属于类,当类被首次加载时,会调用执行静态代码块(且只会执行一次)。
3.如果类中存在静态代码块和构造方法,那么会优先执行静态代码块。
package oo.day04;
/**
* 静态代码块的使用演示:
*/
public class SataicDemo3 {
public static void main(String[] args) {
/**
* 简单理解就是不加静态的是属于对象的,你创建几个对象,那么就执行多少次
*
* 构造方法和静态方法谁先执行呢?
* 静态方法
* 在首次创建对象的时候,是有三个步骤的:
* 1. 先将Coo对应的class字节文件加载到方法区(类被加载静态就执行了)
* 2. 创建对象
* 3. 执行构造方法
* */
// Coo coo = new Coo();
// Coo coo1 = new Coo();
Coo.a = 10;
//这里使用Coo.a 赋值10, 当前我这个代码是加载了这个类的,所以我即使没有创建对象,静态方法也是会被执行的。
}
}
class Coo {
public static int a;//静态变量 ---属于类的
// {
// System.out.println("代码块执行了....");
// }
Coo() {
System.out.println("Coo的构造方法执行了");
}
static { //-----静态代码块
System.out.println("Coo的静态代码块执行了...");
}
}
创建对象执行结果:
不创建对象直接赋值运行结果:
设计:项目中所需要用的图片,都需要进行加载初始化,我们应该单独创建一个类来负责项目资源加载的过程。
在项目包下创建一个类 ImageResources
package cn.tedu.submainre;
import javax.swing.ImageIcon;
/**
* 图片资源加载类:
* 负责加载并初始化项目中锁头需要用到的图片
* */
public class ImageResources {
//ImageIcon 类型 用来存储图片资源的类型
public static ImageIcon battleship;//用来存储战舰图片的变量
public static ImageIcon bomb;//用来存储深水炸弹图片的变量
public static ImageIcon gameover;//用来存储游戏结束图片的变量
public static ImageIcon mine;//用来存储水雷图片的变量
public static ImageIcon minesubm;//用来存储水雷潜艇图片的变量
public static ImageIcon obsersubm;//用来存储侦察潜艇图片的变量
public static ImageIcon sea;//用来存储海洋背景图片的变量
public static ImageIcon start;//用来存储开始游戏图片的变量
public static ImageIcon torpedo;//用来存储鱼雷图片的变量
public static ImageIcon torpesubm;//用来存储鱼雷潜艇图片的变量
/**
* 上面存储的都是空值,需要使用静态代码块初始化赋值
* */
static { //静态代码块,当类被首次加载时,执行代码块中的内容,完成所有图片初始化工作
battleship = new ImageIcon("img/battleship.png");
bomb = new ImageIcon("img/bomb.png");
gameover = new ImageIcon("img/gameover.png");
mine = new ImageIcon("img/mine.png");
minesubm = new ImageIcon("img/minesubm.png");
obsersubm = new ImageIcon("img/obsersubm.png");
sea = new ImageIcon("img/sea.png");
start = new ImageIcon("img/start.png");
torpedo = new ImageIcon("img/torpedo.png");
torpesubm = new ImageIcon("img/torpesubm.png");
}
public static void main(String[] args) { //-----main 测试 后删掉即可 数据为8代表正常
System.out.println(battleship.getImageLoadStatus());
System.out.println(bomb.getImageLoadStatus());
System.out.println(gameover.getImageLoadStatus());
System.out.println(mine.getImageLoadStatus());
System.out.println(minesubm.getImageLoadStatus());
System.out.println(obsersubm.getImageLoadStatus());
System.out.println(sea.getImageLoadStatus());
System.out.println(start.getImageLoadStatus());
System.out.println(torpedo.getImageLoadStatus());
System.out.println(torpesubm.getImageLoadStatus());
}
}
常量
用static final 修饰的变量 称之为常量,应用率较高。
常量特点:通过类名访问,不能二次修改。
常量命名:全大写。
常量的适用性:有一份数据,不会变化,且经常使用。
package oo.day04;
import java.sql.SQLOutput;
/**
* 常量使用演示
* */
public class StaticFinalDemo {
public static void main(String[] args) {
// 访问步骤:
//1. 会先将Doo类对应的.class字节码文件加载到方法区
//2. 获取防范区中静态变量b的数据
// System.out.println(Doo.b); //访问静态变量
//访问常量时,编译期间, 会将使用到的常量直接转换为常量里具体的数据
//类似直接打印 System.out.println(123456);
System.out.println(Doo.D); //访问常量
}
}
class Doo {
public int a; //实例变量
public static int b = 100; //静态变量
public final int c = 123; // 用final修饰的变量
public static final int D = 123456; //常量
static {
System.out.println("类被加载了");
}
}
静态运行结果:
常量运行结果:
常量用于一些固定的值,如java中的π
常量的适用性:例如 我们的游戏窗口大小是固定的,而且其它地方也需要频繁用到这些数据,如果一旦窗口大小发生改变,那么用过这些数据的地方都要变,不利于程序维护。我们可以使用常量来表示窗口大小,易于使用降低出错几率。
在GameWorld类内部上方,加上两个常量。
public static final int WIDTH = 641;/** 窗口的宽 */
public static final int HEIGHT = 479; /** 窗口的高 */
在GameWorld的paintWorld方法中,将使用到的宽高具体数据修改为定义好的常量。
在SeaObject类中为潜艇提供的构造方法中用到的高的数据修改使用常量。
总结设计规则
1.将类别下对象共有的属性和行为封装到一个模板类中,可以批量产生对象。
2.将所有(概念统一的)类共有的属性和行为,提取到父类中 (泛化) ,可以复用优化代码
3.在父类中提取的子类的方法,如果子类的具体实现逻辑都一样,那么就设计为普通方法。
若父类中提取的子类共有的方法,子类都有不同是实现逻辑,那么就应该设计为抽象方法。
抽象方法
1.用abstract修饰的方法,为抽象方法。
2.抽象方法必须存在于抽象类中且抽象方法不能有方法体。
3.抽象方法是必须要让子类重写实现的。
抽象类
1.用abstract修饰的类,为抽象类。
2.抽象类只是在原有普通类的基础上可以放抽象方法。
3.抽象类是需要让子类继承实现的。
4.抽象类是不能被创建对象的!
但是可以创建数组对象,实现向上造型
抽象类的意义:
1.封装子类共有属性和行为 ------代码复用
2.可以向上造型—调用父执行子。
子类的行为逻辑不同,但是我们父类中写的抽象方法,可以在实现向上造型时,通过父类的统一的方法入口,来执行不同的子类的具体实现。
- 普通方法也可以统一入口,那做成抽象方法有必要吗?
答:遵循面向对象的设计原则,在当前认为父类中的逻辑子类无法使用,也就意味着子类是必须要重写实现的。如果做成普通方法,很有可能忘记重写,会导致出问题,那么做成抽象方法,语法上约束子类必须重写才可以。
- 子类都重写了父类的方法,那么父类的抽象为什么不能删?
答:父类中的抽象方法,本质上还是所有子类的共性行为,就是为了实现向上造型时,可以通过这个统一入口来管理子类的行为,具体执行时,则是不同的子类的重写逻辑。
画对象到窗口中只需要两个步骤。
1.对象的图片
2.绘制到的坐标位置
@Override
// g 是画笔的意思
public void paint(Graphics g) { //系统提供绘制方法
// 绘制战舰图片到窗口中
ImageIcon icon = ImageResources.battleship; //获取战舰图片
//1. null 2. g 3. x坐标 4. y坐标
icon.paintIcon(null, g, ship.x, ship.y);
}
贴切到项目中画对象
需要5个步骤
第一个步骤:
每个对象都需要获取图片,获取图片的行为应该由本类完成,意味每个类都要写一个获取图片的方法,如果每个类都写了获取图片的行为 那么模板类之间就行为重复了,可以将重复的行为泛化到父类中. 在父类中获取图片的行为方法 应该设计为抽象方法.
protected abstract ImageIcon getImage();
第二个步骤:
在重写获取行为的代码之前,要先设置好状态,每个对象都是有种状态:活着状态或死亡状态.这种状态称之为固定状态(常量).在对象调用获取图片的方法时,需要判断一下当前对象的状态(普通变量).
public static final int LIVE = 0; //活着的状态
public static final int DEAD = 1; //死亡状态
public int currentState = LIVE; //默认当前状态活着
在SeaObject类中,添加两个普通方法,用来判断是否是活着的方法和判断是否是死亡的方法.
/** 判断当前调用该方法的对象 是否是活着的状态 */
protected boolean isLive() {
return this.currentState == LIVE;
}
/** 判断当前调用该方法的对象 是否是si亡的状态 */
protected boolean isDead() {
return this.currentState == DEAD;
}
第三个步骤:
子类重写实现父类的getImage方法。
@Override
protected ImageIcon getImage() { //战舰比较特殊,并不是一打就死,而且当战舰死亡的时候游戏就结束了。
return ImageResources.battleship;
}
@Override //此方法一定会是深水炸弹对象调用的。那么就需要判断当前对象状态 再决定是否返回图片。
protected ImageIcon getImage() {
if (this.isLive()) { //如果当前对象是或者的状态
return ImageResources.bomb; //返回深水炸弹图片
}
return null; //如果能执行到这一行,则表示当前对象是死亡状态,返回null。
}
@Override
protected ImageIcon getImage() {
if(this.isLive()){
return ImageResources.mine;//返回水雷图片
}
return null;//如果代码能走到这一行,返回null
}
@Override
protected ImageIcon getImage() {
if(this.isLive()){
return ImageResources.minesubm;//水雷潜艇图片
}
return null;//如果代码能走到这一行,返回null
}
@Override
protected ImageIcon getImage() {
if(this.isLive()){
return ImageResources.torpedo;//返回鱼雷图片
}
return null;//如果代码能走到这一行,返回null
}
@Override
protected ImageIcon getImage() {
if(this.isLive()){
return ImageResources.torpesubm;//返回鱼雷潜艇图片
}
return null;//如果代码能走到这一行,返回null
}
@Override
protected ImageIcon getImage() {
if(this.isLive()){
return ImageResources.obsersubm;//返回侦察潜艇图片
}
return null;//如果代码能走到这一行,返回null
}
第四个步骤:
每个对象也需要对象绘制图片.绘制的行为放到子类中冗余,那么就可以提取父类中做一个绘制的方法。
/**
* 因为每个子类都需要绘制,那么就将绘制的行为提取到父类中
* 因为每个子类绘制逻辑都一样,所以设计一个普通方法
* 参数需要一个画笔 让外部调用本方法时传递进入即可
*/
public void paintImange(Graphics g) {
ImageIcon icon = this.getImage(); //获取当前调用方法对象的图片
if (icon != null) {
icon.paintIcon(null, g, this.x, this.y);
}
}
第五个步骤:
测试,在GameWorld类的paint方法中:
@Override
// g 是画笔的意思
public void paint(Graphics g) { //系统提供绘制方法
// // 绘制战舰图片到窗口中
// ImageIcon icon = ImageResources.battleship; //获取战舰图片
// //1. null 2. g 3. x坐标 4. y坐标
// icon.paintIcon(null, g, ship.x, ship.y);
ship.paintImange(g);
for (int i = 0; i < submarines.length; i++) {
submarines[i].paintImange(g);
}
}
效果图:
问题:运行游戏后,潜艇可以绘制出来,但是不能移动.潜艇的移动是自动发生的.
先学习
成员内部类(应用率不高)
类中套个类,外层类称之为外部类,内层类称之为内部类.
1.内部类除了外部类可以访问以外,其他地方不具备可见性.
2.内部类对象可以在外部类进行创建.
3.内部类共享外部类的属性和行为 ----包括私有成员
4.内部类访问外部类 类名.this.xx.
package oo.day05;
/**
* 成员内部类的使用演示
*/
public class TestDemo {
public static void main(String[] args) {
// Boo b = new Boo(); 内部类对外部其它类具备不可见
}
}
class Aoo { //外部类
private int a = 10;
private void show() {
Boo boo = new Boo(); // 外部类中可以创建内部类Boo 对象
}
class Boo { //内部类
int a = 10;
void test() {
/**
* 外部类和内部类变量同名时 会根据就近原则,选择近的,如果想要访问外部类的变量
* 那么就使用类名.this.变量 (这里使用场景是变量同名,一般正常使用即可)
* */
Aoo.this.a = 10; //--代表访问Aoo类的a
Aoo.this.show();//代表访问Aoo类的show方法
a = 20; // 内部类共享外部类的属性和行为--包括私有属性
show();
}
}
}
匿名内部类(应用率高)
没有名字的内部类,称之为匿名内部类.
适用性**😗*
如果一个子类,仅仅只是想重写实现父类中的某个功能时,且其他位置都不需要用到这个子类,那么我们可以直接匿名内部类方法直接重写实现逻辑即可. 更关心的重写的逻辑,而不是重写的步骤.
1.匿名内部类只会存在于子类要重写父类(抽象/普通)方法时使用。
2.匿名内部类,使用外部类的变量时,默认是用final修饰的。
package oo.day05;
import org.w3c.dom.ls.LSOutput;
/**
* 匿名内部类的使用:
* */
public class NsInnerClassDemo {
public static void main(String[] args) {
int a = 10;
// a = 20;
// 使用匿名内部类的方式
// 运行过程:
// 1. 创建SuperClass的子类 只不过没有名字(匿名内部类)
// 2. 将当前创建的子类对象赋值给s1这个变量。
// 3. 花括号则是子类的类体。
SuperClass s1 = new SuperClass(){ //匿名内部类的创建方式
// 匿名内部类是superclass的子类 匿名这个类是NsInnerClassDemo内部类
//当内部类去访问外部类的属性,那么就会默认使用为final来修饰,不能二次修改
@Override
public void show() {
System.out.println(a); //如果外部类对a 这个变量进行修改了,那么就会报错:从内部类引用的本地变量必须是最终变量或实际上的最终变量
System.out.println("通过匿名内部类的方式来实现重写");
}
};
s1.show();
SuperClass s = new SubClass();
s.show();
}
}
// 1. 创建类
// 2. 实现继承
// 3. 实现重写
class SuperClass{ //父类
public void show() {
System.out.println("SuperClass类的show方法");
}
}
class SubClass extends SuperClass { //子类
@Override
public void show() {
System.out.println("SubClass类的show方法");
}
}
面试题:
问:内部类有没有独立的.class字节码文件?
答:有,不管是成员内部类和是匿名内部类,都有独立的.class字节码。
动态入场
潜艇入场 -----------------------------自动发生
雷(水雷和鱼雷)入场 -----------------------------自动发生
如何自动发生
1.定时器
2.通过线程来解决(第二阶段)
定闹钟
1.给闹钟定一个任务 2.开始延时多久执行这个闹钟任务 3.闹钟任务执行一次以后距离下次执行的间隔时间。
定时器
1.具体执行的任务 2.延时多久开始执行 3.执行一次后距下一次的间隔时间。
Java提供了任务模板类/ 定时器模板类
在GameWorld类上方导入 这两个功能。
import java.util.TimerTask;//任务模板
import java.util.Timer;//定时器模板
潜艇入场生成数量是不确定的,没有办法在编译期间确定产生的数量。应该在运行以后不断产生潜艇对象。
数组扩容
数组扩容本质上是创建了新的数组对象,并不在原有基础上扩容,因为数组一旦完成初始化容量,是长度固定 大小不可变。
package oo.day05;
import java.lang.reflect.Array;
import java.util.Arrays;
/**
* 数组拷贝的使用演示类
* 1. Arrays.copyof(); -----更多是基于在源数组基础上实现扩容/缩容情况下使用。
* 2. Ssystem.arraycopy(); -----更多是在已存在两个数组基础上进行拷贝的工作.例如 将A数组的内容拷贝B数组中。
*/
public class ArrayCopyDemo {
public static void main(String[] args) {
// Arrays.copyof()的使用
int[] array = {}; //位array = new int[0]
int a = 10;
System.out.println("扩容之前的数组长度为: " + array.length); // 0
// 1. 要处理的数据是哪个 2. 基于源数组的长度 进行缩松或者扩容的容量
array = Arrays.copyOf(array, array.length + 1);
/**
* 这里数组确定了个数之后,就不能改变的copyOf本质是在堆中产生了新数组对象比原数组多一个容量
* 并将原来数组的元素复制到新数组中。新数组的内存地址和原数组是不同的
* 之后将新的内存地址指向原先的array
* */
array[0] = a;
System.out.println("扩容之后的容量:" + array.length);
System.out.println(array[0]);
int[] array2 = {10, 20, 60, 50};
array2 = Arrays.copyOf(array2, array2.length + 1);
array2[array2.length -1] = 80; //为数组最后一个元素赋值
for (int i = 0; i < array2.length; i++) {
System.out.println(array2[i]);
}
System.out.println("*********************");
// 2. Ssystem.arraycopy()的使用
int[] array3 = {50,60,80,90};
int[] array4 = {1,1,1,1};
/**
* arraycopy 需要五个参数
* 1. 要拷贝的原数组
* 2. 要拷贝的元素组开始的下标
* 3. 你要拷贝的目标数组
* 4. 从目标数组下标的哪个下标开始装
* 5. 拷贝过来的数量是多少 (数组长度 - 不拷贝的元素数量)
* */
System.arraycopy(array3, 1, array4, 0, array3.length - 2);
for (int i = 0; i < array4.length; i++) {
System.out.println(array4[i]);
}
}
}
潜艇入场两个步骤:
1.实现生成潜艇对象的代码,需要做一个生成潜艇对象的方法。
/**
* 生成潜艇对象的方法 返回值可以写具体潜艇类型吗? 不能
* 如果写了具体潜艇类型 方法就不通用了
* 返回值应该写SeaObject 类型 父类型代表不同的子类型
* */
public SeaObject creatSubmarine() {
/**
* 1. 产生0-20的随机数
* 2. 如果生成随机数在0-9 区间 返回侦察潜艇对象OS
* 如果生成随机数在10-14区间 返回水雷潜艇对象 MS
* 如果生产随机数在15-19 区间 返回鱼雷潜艇对象 TS
* */
int number = (int) (Math.random() * 20);
if (number < 10) {
return new ObserverSubmarine();
} else if (number <15) {
return new MineSubmarine();
} else {
return new TorpedoSubmarine();
}
}
2.做一个潜艇入场的方法:方法内部调用createSubmarine方法,生成出的潜艇对象将对象 存放在动态扩容的潜艇数组中即可。
/**
* 潜艇入场方法 ----改防范放在run中调用
*/
public void submarineEnterAction() {
/**
*
* 1. 调用生成潜艇的方法并接收对象
* 2. 将潜艇数组扩一个容量
* 3. 将生成的潜艇对象赋值给 潜艇数组扩容的位置
* */
SeaObject seaObject = creatSubmarine();
submarines = Arrays.copyOf(submarines, submarines.length + 1);
submarines[submarines.length - 1] = seaObject;
}
3.paint方法里 遍历潜艇数组并绘制数组中每个对象…即可实现潜艇入场(在屏幕中)
控制潜艇入场速度:
/**
* 潜艇入场方法
*/
private int submarineEnterIndex = 0; //潜艇生成速度的索引
public void submarineEnterAction() { //每10毫秒调用一次
/**
*
* 1. 调用生成潜艇的方法并接收对象
* 2. 将潜艇数组扩一个容量
* 3. 将生成的潜艇对象赋值给 潜艇数组扩容的位置
* */
submarineEnterIndex++; // 自增
if (submarineEnterIndex % 200 == 0) { //200毫秒执行一次
SeaObject seaObject = creatSubmarine();
submarines = Arrays.copyOf(submarines, submarines.length + 1);
submarines[submarines.length - 1] = seaObject;
System.out.println("对象产生");
}
}
雷入场的实现步骤:
1.需要生成雷对象的方法(水雷/鱼雷)
水雷和鱼雷分别是由水雷潜艇和鱼雷潜艇生成的,生成雷的逻辑是一样的,我们可以在父类中写一个专门为潜艇提供的生成雷的方法,实现复用。
/**
* 生成发射雷方法----返回值是父类,因为返回的并不是一种雷
* shootThunders方法只会被潜艇对象调用
*/
protected SeaObject shootThunders() {
int x = this.x + this.width;//雷对象的x坐标
int y = this.y - 5; //雷的y坐标
//instanceof 用来判断当前对象是否是某个类型的判断语句
if (this instanceof TorpedoSubmarine) { //如果当前对象是鱼雷潜艇类型的话
return new Torpedo(x, y); // 返回鱼雷对象
} else if (this instanceof MineSubmarine) { //如果当前对象是水雷潜艇类型的话
return new Mine(x, y); //返回水雷
}
return null; //如果代码能走到当前位置这段代码,那么就返回null
}
2.雷入场的实现 放到run中调用
/**
* 雷入场的方法------放到run中执行
*/
private int thunderEnterIndex = 0; //控制雷产生的速度
public void thunderEnterAction() {
/**
* 1. 循环便利潜艇数组并调用数组中每个对象的shootThunder方法 并接受雷对象
* 2. 判断雷对象是否为空
* 3. 不为空,才能将雷数组扩容
* 4. 将雷对象赋值给扩容位置。
*
*/
thunderEnterIndex++;
if (thunderEnterIndex % 100 == 0) { // 1秒
for (int i = 0; i < submarines.length; i++) {
SeaObject seaObject = submarines[i].shootThunders();
if (seaObject != null) { //如果雷对象不为空
thunders = Arrays.copyOf(thunders, thunders.length + 1);
thunders[thunders.length - 1] = seaObject;
}
}
}
}
潜艇/雷/深水炸弹的移动工作都是自动发生的:
所有的潜艇 x 向右运行 : x += speed;
所有的雷(水雷/鱼雷) y向上运动: y -= speed;
深水炸弹 y向下运动: y += speed;
移动是自动发生的,在GameWorld类中写一个 用来实现需要调用自动移动的对象对应的step方法。
/**
* 实现调用需要自动移动的对象setp方法----放在run中调用
*/
public void setpAction() {
//遍历潜艇数组 调用每个对象的step方法
for (int i = 0; i < submarines.length; i++) {
submarines[i].step();
}
//遍历雷数组 调用每个对象的step方法
for (int i = 0; i < thunders.length; i++) {
thunders[i].step();
}
//遍历深水炸弹数组 调用每个对象的step方法
for (int i = 0; i < bombs.length; i++) {
bombs[i].step();
}
}
手动发生:
战舰的移动 ------------- 通过按下键盘 ← →键实现移动
深水炸弹的生成 ---------通过按下键盘 空格键实现 生成
事件:发生了一件事情 (去餐厅点餐)
事件处理:发生事件后要做的事情(点餐后让服务员通知取餐)
侦听:用来检测事件处理的条件是否达成 (服务员实时听有没有出餐)
1.当按下键盘! 2.生成深水炸弹 3.计算机实时监听 键盘空格键是否被按下
Java中提供了键盘侦听器和事件
在GameWorld中导入两个功能:
import java.awt.event.KeyEvent;//键盘事件
import java.awt.event.KeyAdapter;//键盘侦听器
在action方法中测试使用:
private void action() { //将声明的变量对应的对象都创建出来
//实现监听 监听的内容(条件)也需要实现
KeyAdapter adapter = new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
// 当按下了键盘的空格键 ---- 1. 用户当下按下的键
// 参数e代表用户按下的键 e.getKryCode() 获取用户按下的键
if (e.getKeyCode() == KeyEvent.VK_SPACE) { //判断用户是否按下空格键
System.out.println("按下了空格");
}
深水炸弹入场
1.在战舰类中写一个发射深水炸弹的方法
/**
* 当战舰对象调用了该方法 则返回一个深水炸弹
* */
public Bomb shootBomb() {
return new Bomb(this.x, this.y); //返回当前深水炸弹对象 生成的坐标取决于战舰的坐标
}
2.在GameWorld类中写一个深水炸弹入场的方法
/**
* 深水炸弹入场 ----自动发生应该在当按下键盘空格键的语句中调用
* */
public void bombEnterAction() {
// 1. 调用发射深水炸弹方法接受对象
Bomb bomb = ship.shootBomb();
// 2. 为深水炸弹数组扩容
bombs = Arrays.copyOf(bombs, bombs.length + 1);
// 3. 将对象 赋值给数组扩容的位置。
bombs[bombs.length - 1] = bomb;
}
3.在action方法中的键盘侦听逻辑但按下键盘空格键的位置调用bombEnterAction
//实现监听 监听的内容(条件)也需要实现
KeyAdapter adapter = new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
// 当按下了键盘的空格键 ---- 1. 用户当下按下的键
// 参数e代表用户按下的键 e.getKryCode() 获取用户按下的键
if (e.getKeyCode() == KeyEvent.VK_SPACE) { //判断用户是否按下空格键
bombEnterAction();
}
}
};
this.addKeyListener(adapter); //将监听器对象添加到检测当中
战舰的移动是手动发生的.
1.在战舰类中写两个方法 用来处理左移和右移
public void leftMove() { // 左键方法
this.x -= speed;
}
public void righMove() { //右移方法
this.x += speed;
}
2.在action方法中添加对应判断左右键的语句并调用对应的移动方法
if (e.getKeyCode() == KeyEvent.VK_LEFT) { //判断是否按下左键
ship.leftMove(); //调用左移方法
}
if (e.getKeyCode() == KeyEvent.VK_RIGHT) { //判断是否按下右键
ship.righMove(); //调用右移方法
}
问题:当屏幕中的对象 移出屏幕外,这些对象本质上还是存在于内存中。但是我们认为已经移出屏幕外的对象,已经没有存在的意义了。
400毫秒 1个潜艇
1秒 2个潜艇 + 2个雷 -----4个对象
1分钟 -------240对象
10分钟 -------2400个对象
10毫秒 移动方法里 要遍历2400个对象 绘制方法 要遍历 2400个对象 ------4800对象
1分钟 ------处理4800000个对象了!
优化内存
内存泄漏:指的就是在运行过程中,不断创建新对象.
内存溢出:指的就是内存中没有可以使用的空间了!(报内存溢出异常!)
垃圾回收器:GC 处理的就是内存垃圾(没有被引用的对象)
项目中:只要是移出到屏幕外的对象 都是内存垃圾。
在父类中写一个判断是否越界的方法,后续该方法会被需要的对象所调用。
/***
* 是否越界方法
* 为什么不做抽象方法而做普通方法?
* 因为三种潜艇判断是否越界的标准是一样的。可以复用
* 其它不能复用,深水炸弹,鱼雷水雷不一样则重写即可。
*/
protected boolean isOutBounds() {
return this.x >= GameWorld.WIDTH; //判断潜艇的x是否超出屏幕的宽
}
深水炸弹类重写:
@Override
protected boolean isOutBounds() {
return this.y >= GameWorld.HEIGHT; //深水炸弹对象的y大于等于屏幕的高
}
---------------------------------------------
水雷类重写:
@Override
protected boolean isOutBounds() {
return this.y <= 150 - this.height;//如果水雷对象的y小于或等于150 - 自身的高,说明到水面上了
}
---------------------------------------------
鱼雷类重写:
@Override
protected boolean isOutBounds() {
return this.y <= -this.height;//如果当前鱼雷对象的y 小于等于 -自身的高度
}
判断并删除是否越界的对象的行为是自动发生的,所以应该在GameWorld类中写一个方法。在run中调用
/**
* 判断并删除越界对象的方法-------
* */
public void outOfBounds() {
// 1. 遍历潜艇数组
// 2. 在循环里依次访问判断每个对象的isOutBounds方法
// 3. 若当前对象判断是否越界为true
// 4. 可以将当前数组中最后一个元素赋给 当前越界对象位置(submarine[i])
// 5. 缩容---最后一个元素 数组length-1 影响原数组
for (int i = 0; i < submarines.length; i++) {
if (submarines[i].isOutBounds()) {
submarines[i] = submarines[submarines.length - 1];
submarines = Arrays.copyOf(submarines, submarines.length -1);
}
}
// 遍历删除越界的雷数组中的对象
for (int i = 0; i < thunders.length; i++) {
if (thunders[i].isOutBounds()) {
thunders[i] = thunders[thunders.length - 1];
thunders = Arrays.copyOf(thunders, thunders.length - 1);
}
}
// 遍历删除越界的深水数组中的对象
for (int i = 0; i < bombs.length; i++) {
if (bombs[i].isOutBounds()) {
bombs[i] = bombs[bombs.length - 1];
bombs = Arrays.copyOf(bombs, bombs.length - 1);
}
}
}
在run中调用:
public void run() {//run方法中放置自动发生时需要执行的逻辑
submarineEnterAction();//调用潜艇入场的逻辑
thunderEnterAction();//调用雷入场的逻辑
stepAction();//调用自动移动的对象 方法
outOfBounds();//调用判断删除越界的方法
System.out.println(“潜艇的数量” + submarines.length);
repaint();//重新绘制
}
后续项目需求:
加分的逻辑:
当深水炸弹打到侦察潜艇 + 10分
鱼雷潜艇 + 40分
加命的逻辑:
当深水炸弹打到水雷潜艇 +1条命
接口
定义:
接口是一组行为的规范,接口并不关心多个具体的实现子类是否是一种的关系,更关注的是他们的行为是否达到一致。
一旦子类(实现类)实现接口,那么该子类必须实现接口中所有的(抽象)行为,所有往往设计的时候,一般接口只放一个行为(小而精)。
如图:
理解:
假设中间的是炸弹,炸弹爆炸时,这些物品或者是人都会被炸飞,但是这些被炸飞的类并不是一种关系(在概念中都不同),如何在每个类中都写上这个行为,那么代码就存在冗余,那么接口的作用就是提供一个统一的入口,所有被炸飞的物品或人(类)并不需要自己实现被炸飞的行为,他们只需要实现“炸飞”这个接口就可以了。这样,无论是什么物品或人,只要实现了这个接口,就能被炸弹炸飞
1.接口是一种数据类型(引用类型)
2.用interface来定义接口
3.接口中只能放抽象方法和常量,接口中定义的内容 默认访问权限就是公开的!
4.接口不可以被创建对象(抽象的)
5.接口一旦被设计出,需要子类(实现)来实现的!------>接口中的行为是必须全部被实现类实现。
- 实现类要想实现某个接口 需要用 implements 来实现
例如:实现类名 implements 接口名
若一个类,又想继承类又想实现接口
例如: 子类 extends 父类 implements 接口
7.接口与接口之间 是可以继承的! 例如:B接口 继承了A接口,那么C类实现了B接口,也需要把B接口继承得来的抽象内容一并实现。
package oo.day06;
//接口的使用演示
public class InterfaceDemo {
public static void main(String[] args) {
// Inter1 inter1 = new Inter1(); 接口不能 被实例化对象
Inter1 inter1 = new Inter1() {
@Override
public void test() {
System.out.println("创建匿名内部类");
}
};
}
}
interface Inter1 { //接口 Inter1
//接口中声明的变量 默认就是常量 不用加public static final也可以
int NUMBER = 10;
// 接口中声明的方法默认为抽象方法不用加abstract也可以
abstract void test();
}
interface Inter2 extends Inter1{ // 接口可以继承接口
}
class Aoo {
}
class Boo extends Aoo implements Inter2{
// 这里Inter2接口继承了Inter1 那么Inter2就有Inter1的属性和行为,所有必须重写test()才不会报错
@Override
public void test() {
}
}
/**
* 类与类 可以继承(继承关系)
* 接口与接口 可以继承(继承关系)
* 类与接口 实现关系
* */
面试题:接口与抽象类的区别
抽象类可以放构造方法 ,接口不可以。
抽象类可以放普通成员 , 接口不可以。
抽象类的成员可以加访问修饰符,接口只能是public。
接口不可以实现接口,但是可以继承接口。
一个类只能继承一个类, 但一个类可以实现多个接口.
JDK1.8之前接口只能放抽象方法,现在可以加静态的方法。
注意:
若代码的冗余是局部的逻辑冗余,则提取出方法来复用
若是对个类之间的代码冗余,考虑多个类之间是一种关系吗?
如果是:用类的继承
如果不是行为的重复 用接口。
在项目包下创建两个接口
EnemyScore(加分)接口,对应侦察潜艇和鱼雷潜艇类实现
package cn.tedu.submainre;
/**
* 加分的接口 ---具体由需要加分的类 来具体实现
* */
public interface EnemyScore {
/**
* 提供获取分数的方法
* @return 被打到时 返回的分数
*/
int getScore();
}
侦察潜艇
@Override
public int getScore() {
return 10; // 当侦察潜艇被打到获得10分
}
鱼雷潜艇类:
@Override
public int getScore() {
return 40;
}
EnemyLife(加命接口) 水雷潜艇实现当前接口
package cn.tedu.submainre;
/**
* 加命的接口: 提供加命的行为 具体由实现类决定 返回的命数
* */
public interface EnemyLife {
int getLife();
}
水雷潜艇:
@Override
public int getLife() {
return 1;
}
总结设计规则
-
将类别下对象共有的属性和行为封装到一个模板类中,可以批量产生对象。
-
将所有(概念统一的)类共有的属性和行为,提取到父类中 (泛化) ,可以复用优化代码
-
在父类中提取的子类的方法,如果子类的具体实现逻辑都一样,那么就设计为普通方法。
若父类中提取的子类共有的方法,子类都有不同是实现逻辑,那么就应该设计为抽象方法。
- 接口:(不同类别,或同类别)他们的部分类中存在一些共性的行为且这些行为的内部实现全都不一样,可以通过使用接口来进行代表他们的行为.
错误的分类:
1.编译错误:编译期间产生的错误 -------------全都语法的错误
2.运行错误:运行期间产生的错误 -------------空指针异常/数组下标异常
3.程序不报错,不异常,但是就是跟现象不一样.
解决方案:
1.通过现象,锁定可能出错的代码!
2.打桩(打印测试信息)
多态
同一类型下的不同实现。
人类在睡觉行为上的多态: 右侧睡,左侧睡,平躺睡,趴着睡
人类在个体上的多态:有的人高,有的人低,有的人胖,有的瘦
根据上面的说明,可以得出多态也就是一个方法实现多种效果
程序中的多态 -----行为的多态,同一类型下行为的不同实现。
人 p = new 理发师();//向上造型 声明父 new 子
人 P1 = new 医生();
人 p2 = new 园丁():
向上造型本质上就是一种多态的展现形式
p.cut();//在编译期间p 打点调用的 父类方法 ,受类型制约,具体运行时执行对象的方法
p1.cut();//在编译期间p1 打点调用的 父类方法 ,受类型制约,具体运行时执行对象的方法
p2.cut();//在编译期间p2 打点调用的 父类方法 ,受类型制约,具体运行时执行对象的方法
abstract class 人{
abstract void cut();
}
class 理发师 extends 人 {
void cut(){
System.out.println("剪发");
}
}
class 医生 extends 人 {
void cut(){
System.out.println("做手术");
}
}
class 园丁 extends 人 {
void cut(){
System.ouintln("剪草");
}
}
当一个对象被造型为不同的类型时**,**具有的行为是不同的
讲师 o1 = new 我(); //向上造型
其它老师同事 o2 = new 我();
父母的儿子 o3 = new 我();
o1.授课();
o2.互相卷();
o3.孝顺父母();
interface 讲师{
授课();
}
interface 其他老师的同事{
互相卷();
}
interface 父母的儿子{
孝顺父母();
}
class 我 implements 讲师,其它老师同事,父母的儿子 {
授课(){ }
互相卷(){ }
孝顺父母(){ }
}
向上造型 / 引用类型中的自动类型转换
1.父 大 子 小 ----声明父 new 子
2.如何能够向上造型成功 -------- 父类 new 子对象 / 接口 new 实现类
向下转型/引用类型中的强转类型转换
能否强制类型转换成功,需要看两个条件:
1.要强制转换的引用类型变量中的对象,就是要转换的这个类型
2.要强制转换的引用类型变量中的对象,实现了要转换的这个接口。
在程序中,引用类型一旦出现需要强转的地方,无论是否清楚转换失败或成功,都需要加上instanceof来判断能否强转**,**避免失败!
Aoo a = new Boo(); //声明父 new 子
Boo b = (Boo)a; //可以强制转换成功,符合条件 1
Inter1 c = (Inter1)a;//可以强制转换成功,符合条件 2
Coo c1 = (Coo)a;//类型强制转换失败 : classCastException
要想实现强制转换引用数据类型,要么是a里面存储的Boo这个类型,要么就是a中存储的Boo实现了这个接口
class Aoo{//父类
}
interface Inter1{
}
class Boo extends Aoo implements Inter1 {//子类
}
class Coo extends Aoo{
}
注意:向下转型通常都是发生在有继承关系类之间
package oo.day06;
/**
* 向下转型---引用类型中的强制类型转换
* */
public class ClassCastDemo {
public static void main(String[] args) {
Aoo1 aoo1 = new Boo1(); //向上造型
Boo1 boo1 = (Boo1) aoo1; //将aoo1 的变量类型 强制转换为Boo1类型, 强制转换成功 因为aoo1中对象是Boo1这个类型
InterDemo interDemo = (InterDemo) aoo1; //将aoo1的变量类型 强制转换为InterDemo接口类型 强制转换成功,因为aoo1中对象实现了InterDemo接口
if (aoo1 instanceof Coo){ //判断 aoo1是否可以转换为Coo这个对象(也可以理解成aoo1是不是Coo类型,如果存在继承关系那么是),引用类型强转建议写instanceof来判断
Coo c1 = (Coo) aoo1; //强转失败! 因为aoo1中对象既不是Coo这个类型, 也没有实现Coo这个接口的操作
System.out.println("强转成功");
} else {
System.out.println("不能强转");
}
}
}
class Aoo1{//父类
}
interface InterDemo{
}
class Boo1 extends Aoo1 implements InterDemo {//子类
}
class Coo extends Aoo1{
}
package oo.day06;
public class TestDemo {
public static void main(String[] args) {
Eoo eoo = new Eoo();
if (eoo instanceof Doo){
/**
* Eoo 是 Doo 的子类。instanceof 关键字用于测试一个对象是否是指定类型的实例。
* eoo是Eoo类的实例,而Eoo是Doo的子类。因此,eoo也可以被视为是Doo类型的实例。
* */
System.out.println("yes");
}
}
}
class Doo{
}
class Eoo extends Doo{
项目中碰撞:
深水炸弹与潜艇碰撞
雷 与 战舰碰撞
因为碰撞行为子类都有,提取父类中一个碰撞的行为方法,因为碰撞的逻辑是一样的,只是参与碰撞的对象不同,所以做成普通方法 ,碰撞对象可以通过参数传递过来。
//为所有子类提供的碰撞行为
protected boolean isHit(SeaObject other) {
// 计算当前对象与 碰撞物(other)的碰撞领空
int x1 = this.x - other.width;
int x2 = this.x + other.width;
int y1 = this.y - other.height;
int y2 = this.y + other.height;
//获取碰撞物的x 和 y
int x = other.x;
int y = other.y;
//返回 条件 碰撞物的x 在x1和x2之间,y在y1 和y2之间,如果成了 则表示撞倒了当前对象
return (x >= x1 && x <= x2) && (y >= y1 && y<= y2);
1)潜艇对象.isHit(深水炸弹对象) ------------------this:潜艇对象 other:深水炸弹对象
2)深水炸弹对象.isHit(潜艇对象) ------------------this:深水炸弹对象 other:潜艇对象
3)雷对象.isHit(战舰对象) ------------------this: 雷对象 other:战舰对象
4)战舰对象.isHit(雷对象) ------------------this: 战舰对象 other:雷对象
碰撞的行为是自动发生的,在GameWorld类中,做一个方法 bombBangAction,在run中调用。
/**
* 此方法 处理 深水炸弹与潜艇碰撞的行为使用实现----run中
* */
public void bombBangAction(){
for (int i = 0; i < bombs.length; i++) { //控制轮数
Bomb b = bombs[i]; //接收当前需要与潜艇对象挨个碰撞的炸弹
for (int j = 0; j < submarines.length; j++) { //控制次数
if (b.isHit(submarines[j])) { //拿当前炸弹对象 依次与潜艇对象进行碰撞检测
System.out.println("撞上了!");
// b -- 对象 标记为死亡
// submarines[j]----对象标记为死亡
}
}
}
}
深水炸弹打到潜艇,两个对象都要消失,我们可以在SeaObject类中写一个goDead方法,哪个对象打点该goDead方法,那个对象就会被标记为死亡状态
/**
* 父类 判断当前调用该方法的对象 是否是si亡的状态
*/
protected boolean isDead() {
return this.currentState == DEAD; //将当前对象的状态设置为死亡状态
}
鱼雷/水雷与战舰的碰撞具体实现,在GameWorld类中thunderBangAction,放在run中调用
/***
* 雷与战舰的碰撞检测实现---放在run中调用
* 遍历当前 雷数组,依次拿数组 中的每个对象 与战舰对象进行碰撞检测,若碰上 当前对象标记为死亡。
*/
public void thunderBangAction() {
for (int i = 0; i < thunders.length; i++) {
if (thunders[i].isHit(ship)) {
thunders[i].goDead(); //当前雷对象标记为死亡
}
}
}
深水炸弹与潜艇碰撞后,要加分或加命的逻辑
/**
* 此方法 处理 深水炸弹与潜艇碰撞的行为使用实现----run中
* */
public void bombBangAction(){
for (int i = 0; i < bombs.length; i++) { //控制轮数
Bomb b = bombs[i]; //接收当前需要与潜艇对象挨个碰撞的炸弹
for (int j = 0; j < submarines.length; j++) { //控制次数
if (b.isHit(submarines[j])) { //拿当前炸弹对象 依次与潜艇对象进行碰撞检测
b.goDead(); //b -- 对象 标记为死亡
submarines[j].goDead(); //submarines[j]----对象标记为死亡
/**
* 通过instanceof 来判断submarines[j]数组是哪个潜艇对象
* 因为当前submarines数组是SeaObject类型 没有加分加命的操作
* 需要向下转型,转换为具体的潜艇类型才可以调用对应的加分加命的方法
* */
if (submarines[j] instanceof ObserverSubmarine) { // 判断当前被撞到的潜艇对象是否为侦察潜艇对象
ObserverSubmarine os = (ObserverSubmarine)submarines[j]; //强制转为侦查潜艇对象
score += os.getScore(); //执行加分
} else if (submarines[j] instanceof TorpedoSubmarine) {
TorpedoSubmarine ts = (TorpedoSubmarine)submarines[j]; //强转为鱼雷潜艇
score += ts.getScore(); //执行加分
} else if (submarines[j] instanceof MineSubmarine) {
MineSubmarine ms = (MineSubmarine)submarines[j]; //强转为水雷潜艇
ship.setLife(ms.getLife()); //加命
}
}
}
}
}
优化代码:
public void bombBangAction(){
for (int i = 0; i < bombs.length; i++) { //控制轮数
Bomb b = bombs[i]; //接收当前需要与潜艇对象挨个碰撞的炸弹
for (int j = 0; j < submarines.length; j++) { //控制次数
if (b.isHit(submarines[j])) { //拿当前炸弹对象 依次与潜艇对象进行碰撞检测
b.goDead(); //b -- 对象 标记为死亡
submarines[j].goDead(); //submarines[j]----对象标记为死亡
/**
* 通过instanceof 来判断submarines[j]数组是哪个潜艇对象
* 因为当前submarines数组是SeaObject类型 没有加分加命的操作
* 需要向下转型,转换为具体的潜艇类型才可以调用对应的加分加命的方法
* */
if (submarines[j] instanceof EnemyScore) { //判断当前潜艇 对象是否实现EnemScore接口
EnemyScore enemyScore = (EnemyScore)submarines[j];
score += enemyScore.getScore(); //调用父(接口) 执行子
} else if (submarines[j] instanceof EnemyLife) { //判断当前潜艇对象是否实现EnemLife接口
EnemyLife enemyLife = (EnemyLife)submarines[j];
int life = enemyLife.getLife();
ship.setLife(life);
}
}
}
}
//好处:新增加命 加分等行为的潜艇,以下代码不需要改变了. 提高程序复用性,扩展性
理解:
这行代码检查submarines[j]
是否是EnemyScore
的实例,
EnemyScore enemyScore = (EnemyScore) submarines[j];:如果
submarines[j]是
EnemyScore的实例,那么这行代码将执行类型强制转换,将
submarines[j]转换为
EnemyScore类型的对象。这使你可以访问
EnemyScore`类的方法和属性,
score += enemyScore.getScore();
:在这里,你调用enemyScore
对象的getScore()
方法,从中获取得分,并将其添加到score
变量中。这是多态性的一种体现,因为你无需关心实际的对象类型,只需要调用通用的方法,该方法会执行特定对象类型的操作。
战舰类提供减命的行为:
public void subtractLife(){ //提供减命 在战舰类中编写
this.life--;
}
在运行类中调用
/***
* 雷与战舰的碰撞检测实现---放在run中调用
* 遍历当前 雷数组,依次拿数组 中的每个对象 与战舰对象进行碰撞检测,若碰上 当前对象标记为死亡。
*/
public void thunderBangAction() {
for (int i = 0; i < thunders.length; i++) {
if (thunders[i].isHit(ship)) {
thunders[i].goDead(); //当前雷对象标记为死亡
ship.subtractLife();
}
}
}
游戏状态:
-
游戏开始状态:当运行程序后,应该有一个开始游戏的界面,当按下空格键 切换为运行状态
-
游戏运行状态: 潜艇/雷自动生成,移动.战舰发射深水炸弹等等, 当战舰命为0切换结束状态
-
游戏结束状态:当战舰命数为0,游戏结束 有一个对应的界面,当前的画面暂停。
在GameWorld类中 定义3个常量(游戏固定状态),1个变量(当前游戏状态)
public static final int GAME_START = 0; //开始状态
public static final int RUNNING = 1; //运行状态
public static final int GAME_OVER = 2; //结束状态
private int gameCurrentState = GAME_START;
将对应的绘制代码,根据游戏状态进行不同的切换:
@Override
// g 是画笔的意思
public void paint(Graphics g) { //系统提供绘制方法
// // 绘制战舰图片到窗口中
// ImageIcon icon = ImageResources.battleship; //获取战舰图片
// //1. null 2. g 3. x坐标 4. y坐标
// icon.paintIcon(null, g, ship.x, ship.y);
switch (gameCurrentState) {
case GAME_START:
ImageResources.start.paintIcon(null, g, 0, 0);
break;
case RUNNING:
ImageResources.sea.paintIcon(null, g, 0, 0); //绘制背景板
ship.paintImange(g);
//绘制潜艇数组中的每个对象
for (int i = 0; i < submarines.length; i++) {
submarines[i].paintImange(g);
}
//绘制雷数组中的每个对象
for (int i = 0; i < thunders.length; i++) {
thunders[i].paintImange(g);
}
//绘制深水炸弹数组中的每个对象
for (int i = 0; i < bombs.length; i++) { //遍历深水炸弹数组
bombs[i].paintImange(g); //绘制每个对象
}
g.setFont(new Font("", Font.BOLD, 20)); //设置字体大小
g.drawString("Score: " + score, 200, 50 ); //画分
g.drawString("Life: " + ship.getLife(), 400, 50); //画命
break;
case GAME_OVER:
break;
}
1.当按下空格键,如果当前状态为开始状态则切换状态为运行状态:
// 当按下了键盘的空格键 ---- 1. 用户当下按下的键
// 参数e代表用户按下的键 e.getKryCode() 获取用户按下的键
if (e.getKeyCode() == KeyEvent.VK_SPACE) { //判断用户是否按下空格键
if (gameCurrentState == GAME_START){ //如果是开始状态
gameCurrentState = RUNNING; //切换成运行状态
}else{
bombEnterAction(); //调用深水炸弹入场的方法
}
}
2.运行状态下,在进行生成潜艇.雷,移动等交互行为:
@Override
public void run() { //run方法中放置自动发生时需要执行的逻辑
if (gameCurrentState == RUNNING) { //当游戏为运行状态才能执行
submarineEnterAction(); //调用潜艇入场的逻辑
thunderEnterAction(); // 调用雷入场的逻辑
setpAction(); //移动方法
bombBangAction(); //调用深水炸弹与潜艇碰撞检测方法
thunderBangAction(); //雷与战舰碰撞检测方法
outOfBounds(); // 判断删除越界的方法
repaint(); // 从新绘制
}
}
};
3.检测游戏结束的方法
/**
* 检测游戏是否结束-- 这里放在run中执行或者是在战舰碰撞炸弹方法中都可以
* */
public void chekGamOver() {
if (ship.getLife() <= 0) { //当前战舰命数为0
gameCurrentState = GAME_OVER; // 切换为游戏结束状态
}
}
课外作业:
1.战舰移动目前会移出屏幕,如何解决实现?
2.游戏结束,如何重新开始游戏?
基础知识总结
-
声明个变量会不会? 成员变量? 局部变量?
-
不同数据类型知不知道用来存什么数据吗??? String int double char boolean…
-
运算符会不会用?
数学运算符 + - * / %
自增自减运算符: ++ – 作用于变量
单独运算时,符号在前在后都一样
参与运算时,符号在前,先做对应符号操作,然后参与其他运算
符号在后,先参与其他运算,再处理对应符号操作
关系运算符 > < >= <= == !=
逻辑运算符
逻辑与(&&) 并且关系
逻辑或(||) 或者关系
逻辑非 (!) !true ---- false !fasle ----true
短路与,如果前面的条件为false 立刻返回结果false
短路或,如果前面的条件为true 立刻返回结果true
三元表达式:
数值 % 2 == 0 ? “是偶数” : “是奇数”;
if(数值 % 2 == 0){
是偶数
}else{
是奇数
}
字符串拼接符:
“所见即所得” -----String 字符串类型
name
“我的名字” + name
“1” + 1 — “11”
顺序结构:代码逐语句执行
分支结构: 有条件执行某个语句
- 单路分支 :只有一个条件,对应一个执行语句
if(条件){ 成立执行的代码块 }
- 双路分支:只有一个条件,对应有成立执行语句,或不成立执行的语句
if(条件){
成立执行的代码块
} else{
不成立执行的代码块
}
3.多路分支:当对一个数据有多种判定条件时 使用…
if(条件1){
} else if(条件2){
}else if(条件3){
}
swicth(判定的数据){
case 数值1:
break;
case 数值2:
break;
case 数值3:
break;
default:
}
循环结构: 有条件循环某个语句
当不明确循环次数已知循环条件时使用:
while : 先判断条件 再决定是否执行循环体
while( 条件) { 循环的代码}
do…while:先做一次循环体,再判断条件
do { } while(条件)
当明确循环次数时使用
for(int i = 0; i< 5; i++){
}
数组
当有同一类型的多个数据时,用数组来表示
int[] arr1 = {10,20,30}; ----静态初始化 :当做数组时明确数组中的内容
int[] arr2 = new int[3]; —动态初始化 : 先把空间开好
方法
当程序中有一段重复冗余代码,可以提取成方法来复用.
1.无参无返回值方法
2.有参无返回值方法
3无参有返回值方法
4.有参有返回值方法
方法重载:当功能是一样 只是因为参数个数 或参数类型不同,我们可以用一个方法名来代表这些功能.
面向对象:
三大特征:封装, 继承 ,多态
封装 :
类的封装 —当一个类别下有多个对象存在共有的属性或行为,封装成类来代表.
继承:
模板之间有一些共性属性或行为,且是一种 ----人类
多态:
同一类型下的不同实现
医生.老师.学生 重写了对应的sayHi功能
人类 ----- 医生.老师.学生