前言:
前天我们学习了继承,子类可以在父类的基础上重写父类内容,这样很容易误伤很多固定用法,已经写好的类,为了避免这种情况,java提供了final关键字,用来修饰不可改变内容。
我的理解是,类似于我们相机里的上锁,使用final关键字就相当于给一些内容上锁,让他们不会被轻易改变。
今日重点:
final关键字
权限
内部类
引用类型
文章索引:
final关键字
final关键字代表最终、不可改变的。 常见四种用法: 1. 可以用来修饰一个类 2. 可以用来修饰一个方法 3. 还可以用来修饰一个局部变量 4. 还可以用来修饰一个成员变量
修饰类:
当final关键字用来修饰一个类的时候,格式: public final class 类名称 { // ... }含义:当前这个类不能有任何的子类。(太监类) 注意:一个类如果是final的,那么其中所有的成员方法都无法进行覆盖重写(因为没儿子。)
代码示例:
public final class MyClass /*extends Object*/ {
public void method() {
System.out.println("方法执行!");
}
}
修饰方法:
当final关键字用来修饰一个方法的时候,这个方法就是最终方法,也就是不能被覆盖重写。 格式: 修饰符 final 返回值类型 方法名称(参数列表) { // 方法体 } 注意事项: 对于类、方法来说,abstract关键字(抽象方法)和final关键字不能同时使用,因为矛盾。因为抽象方法一定要被子类覆盖重写,然而final关键字代表不能被覆盖重写,他俩矛盾所以不能同时使用。
示例代码:
public final void method() {
System.out.println("方法执行!");
}
修饰局部变量:
只有方法里的才叫局部变量,类里的叫成员变量。
public static void main(String[] args) {
// 一旦使用final用来修饰局部变量,那么这个变量就不能进行更改。
// “一次赋值,终生不变”
final int num2 = 200;
System.out.println(num2); // 200
// 正确写法!只要保证有唯一一次赋值即可
final int num3;
num3 = 30;
引申——什么叫做不可变?
对于基本类型来说,不可变指的是数值不可变,比如int abc= 10,那么abc一辈子就等于10了,而对于引用类型中的不可变,指的是指向地址不可变(他的内容是可以改变的!)。
final Student stu2 = new Student("高圆圆");
// 错误写法!final的引用类型变量,其中的地址不可改变
// stu2 = new Student("赵又廷");
System.out.println(stu2.getName()); // 高圆圆
stu2.setName("高圆圆圆圆圆圆");
System.out.println(stu2.getName()); // 高圆圆圆圆圆圆
这里通过set方法修改了值,但他的引用并没有发生改变,但是内容却改变了。
修饰成员变量:
对于成员变量来说,如果使用final关键字修饰,那么这个变量也照样是不可变。 1. 由于成员变量具有默认值,所以用了final之后必须手动赋值,不会再给默认值了。 2. 对于final的成员变量,要么使用直接赋值,要么通过构造方法赋值。二者选其一。 3. 必须保证类当中所有重载的构造方法,都最终会对final的成员变量进行赋值。
public class Person {
private final String name/* = "鹿晗"*/;
//这里使用了final关键字对他进行修饰,可以直接在后面对他进行赋值。
public Person() {
name = "关晓彤"; //这里通过构造方法对他进行了赋值。
}
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
// public void setName(String name) {
// this.name = name;
// }
//不能在这样写了!!因为与final关键字矛盾!使用了这个关键字只能赋值一次!不能再用构造方法赋值为空
//了!!
}
权限修饰符
在Java中提供了四种访问权限,使用不同的访问权限修饰符修饰时,被修饰的内容会有不同的访问权限,
public:公共的。
protected:受保护的
default:默认的
private:私有的
注意事项:(default)并不是关键字“default”,而是根本不写。
可见,public具有最大权限。private则是最小权限。
编写代码时,如果没有特殊的考虑,建议这样使用权限:
成员变量使用 private ,隐藏细节。
构造方法使用 public ,方便创建对象。
成员方法使用 public ,方便调用方法。
小贴士:不加权限修饰符,其访问能力与default修饰符相同
内部类
内部类说白了就是在一个类里还有一个类,就像身体里包含了心脏。
如果一个事物的内部包含另一个事物,那么这就是一个类内部包含另一个类。 例如:身体和心脏的关系。又如:汽车和发动机的关系。 分类: 1. 成员内部类 2. 局部内部类(包含匿名内部类) 成员内部类的定义格式: 修饰符 class 外部类名称 { 修饰符 class 内部类名称 { // ... } // ... }注意:内用外,随意访问;外用内,需要内部类对象。
示例代码:
public class Body { // 外部类
public class Heart { // 成员内部类
// 内部类的方法
public void beat() {
System.out.println("心脏跳动:蹦蹦蹦!");
System.out.println("我叫:" + name); // 正确写法!
}
}
// 外部类的成员变量
private String name;
// 外部类的方法
public void methodBody() {
System.out.println("外部类的方法");
new Heart().beat();
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
那么如何外部类,如何使用成员内部类呢?
如何使用成员内部类?有两种方式: 1. 间接方式:在外部类的方法当中,使用内部类;然后main只是调用外部类的方法。 2. 直接方式,公式: 类名称 对象名 = new 类名称(); 【外部类名称.内部类名称 对象名 = new 外部类名称().new 内部类名称();】
第一种方法示例:
//在类body中的代码:
public class Heart { // 成员内部类
// 内部类的方法
public void beat() {
System.out.println("心脏跳动:蹦蹦蹦!");
System.out.println("我叫:" + name); // 正确写法!
}
}
// 外部类的方法
public void methodBody() {
System.out.println("外部类的方法");
new Heart().beat();//这里是外部类创建了个匿名对象,间接访问了内部类中的方法
}
//测试类中的代码:
public static void main(String[] args) {
Body body = new Body(); // 创建了一个外部类的对象
// 通过外部类的对象,调用外部类的方法,里面间接在使用内部类Heart
body.methodBody();
}
运行结果:
外部类的方法
心脏跳动:蹦蹦蹦!
我叫:null
第二种方法示例代码:
//测试类中的方法
//公式:【外部类名称.内部类名称 对象名 = new 外部类名称().new 内部类名称();】
Body.Heart heart = new Body().new Heart();
heart.beat();
运行结果:
心脏跳动:蹦蹦蹦!
我叫:null
内部类的同名变量访问 :
如果出现了重名现象,那么格式是:外部类名称.this.外部类成员变量名
//这里是外部类
public class Outer {
int num = 10; // 外部类的成员变量
public class Inner /*extends Object*/ {
int num = 20; // 内部类的成员变量
public void methodInner() {
int num = 30; // 内部类方法的局部变量
System.out.println(num); // 局部变量,就近原则 30
System.out.println(this.num); // 内部类的成员变量 20
System.out.println(Outer.this.num); // 外部类的成员变量 10
}
}
}
//测试类:
public class Demo02InnerClass {
public static void main(String[] args) {
// 外部类名称.内部类名称 对象名 = new 外部类名称().new 内部类名称();
Outer.Inner obj = new Outer().new Inner();
obj.methodInner();
}
}
局部内部类:
如果一个类是定义在一个方法内部的,那么这就是一个局部内部类。 “局部”:只有当前所属的方法才能使用它,出了这个方法外面就不能用了。 定义格式: 修饰符 class 外部类名称 { 修饰符 返回值类型 外部类方法名称(参数列表) { class 局部内部类名称 { // ... } } }
代码示例:
//这里是一个外部类
class Outer {
//这是一个方法
public void methodOuter() {
//这是在方法里的类,所以叫局部内部类
class Inner { // 局部内部类
int num = 10;
public void methodInner() {
System.out.println(num); // 10
}
}
Inner inner = new Inner(); //②再通过外部方法创建内部类对象
inner.methodInner(); //③再用创建好的内部类对象调用局部内部类的方法
}
}
//测试类:
public class DemoMain {
public static void main(String[] args) {
Outer obj = new Outer(); //创建了一个外部类对象
obj.methodOuter(); //①通过外部类调用外部方法,
}
}
类的权限修饰符小结:
public > protected > (default) > private 定义一个类的时候,权限修饰符规则: 1. 外部类:public / (default) 2. 成员内部类:public / protected / (default) / private 3. 局部内部类:什么都不能写
需要注意的是:
局部内部类,如果希望访问所在方法的局部变量,那么这个局部变量必须是【有效final的】。 备注:从Java 8+开始,只要局部变量事实不变,那么final关键字可以省略。 原因: 1. new出来的对象在堆内存当中。 2. 局部变量是跟着方法走的,在栈内存当中。 3. 方法运行结束之后,立刻出栈,局部变量就会立刻消失。 4. 但是new出来的对象会在堆当中持续存在,直到垃圾回收消失。
示例代码:
//这里是个外部类
public class MyOuter {
//外部类中的方法
public void methodOuter() {
int num = 10; // 所在方法的局部变量
//局部内部类
class MyInner {
public void methodInner() {
System.out.println(num);
}
}
}
}
匿名内部类:
匿名内部类 :是内部类的简化写法。它的本质是一个 带具体实现的父类或者父接口的匿名的子类对象。
开发中,最常用到的内部类就是匿名内部类了。以接口举例,当你使用一个接口时,似乎得做如下几步操作:
1. 定义子类
2. 重写接口中的方法
3. 创建子类对象
4. 调用重写后的方法
我们的目的,最终只是为了调用方法,那么能不能简化一下,把以上四步合成一步呢?匿名内部类就是做这样的快捷方式。
使用前提:
匿名内部类必须继承一个父类或者实现一个父接口。
使用格式:
如果接口的实现类(或者是父类的子类)只需要使用唯一的一次, 那么这种情况下就可以省略掉该类的定义,而改为使用【匿名内部类】。 匿名内部类的定义格式: 接口名称 对象名 = new 接口名称() { // 覆盖重写所有抽象方法 };
代码示例:
//这里是一个接口:
public interface MyInterface {
void method1(); // 抽象方法
}
//这里是测试类:
public class DemoMain {
//主方法
public static void main(String[] args) {
// 使用匿名内部类,但不是匿名对象,对象名称就叫objA
MyInterface objA = new MyInterface() {
//这里重写了抽象方法
@Override
public void method1() {
System.out.println("匿名内部类实现了方法!111-A");
}
};
objA.method1(); //通过匿名内部类对象调用了里面的方法
匿名内部类的原理:
对格式“new 接口名称() {...}”进行解析: 1. new代表创建对象的动作 2. 接口名称就是匿名内部类需要实现哪个接口 3. {...}这才是匿名内部类的内容
这里的匿名内部类的内容是写在接口里的!
匿名内部类的注意事项:
1. 匿名内部类,在【创建对象】的时候,只能使用唯一一次。 如果希望多次创建对象,而且类的内容一样的话,那么就需要使用单独定义的实现类了。 2. 匿名对象,在【调用方法】的时候,只能调用唯一一次。 如果希望同一个对象,调用多次方法,那么必须给对象起个名字。 3. 匿名内部类是省略了【实现类/子类名称】,但是匿名对象是省略了【对象名称】 强调:匿名内部类和匿名对象不是一回事!!!
引用类型用法总结
类作为成员变量接口
类里面的成员变量不止可以用基本类型对他进行赋值,也可以用引用类型(比如类)对他进行赋值。
比如:
// 这里是一个类
public class Hero {
private String name; // 英雄的名字 (这里就是基本类型进行赋值)
private int age; // 英雄的年龄 (这里就是基本类型进行赋值)
private Weapon weapon; // 英雄的武器 (这里设置了,使用Weapon对象的方式对他进行赋值)
public Hero() {
}
//这里是一个构造方法
public Hero(String name, int age, Weapon weapon) {
this.name = name;
this.age = age;
this.weapon = weapon;
}
//这里是一个显示方法
public void attack() {
System.out.println("年龄为" + age + "的" + name + "用" + weapon.getCode() + "攻击敌方。");
}
//get set 基本方法
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Weapon getWeapon() {
return weapon;
}
public void setWeapon(Weapon weapon) {
this.weapon = weapon;
}
}
武器类:
public class Weapon {
private String code; // 武器的代号(名字)
public Weapon() {
}
//构造方法
public Weapon(String code) {
this.code = code;
}
//基本的get set
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
}
主程序/测试类:
public class DemoMain {
public static void main(String[] args) {
// 创建一个英雄角色
Hero hero = new Hero();
// 为英雄起一个名字,并且设置年龄
hero.setName("盖伦");
hero.setAge(20);
// 创建一个武器对象,并设置名字为AK47
Weapon weapon = new Weapon("AK-47");
// 为英雄配备武器
hero.setWeapon(weapon);
// 年龄为20的盖伦用AK-47攻击敌方。
hero.attack();
}
}
类作为成员变量时,对它进行赋值的操作,实际上,是赋给它该类的一个对象。
接口作为成员变量
跟上面的类作为成员变量一样,接口也可以作为成员变量。
首先创建一个英雄类:
public class Hero {
private String name; // 英雄的名称
private Skill skill; // 英雄的技能 这里用接口skill对英雄的技能进行定义。
public Hero() {
}
//构造方法
public Hero(String name, Skill skill) {
this.name = name;
this.skill = skill;
}
//展示方法
public void attack() {
System.out.println("我叫" + name + ",开始施放技能:");
skill.use(); // 调用接口中的抽象方法
System.out.println("施放技能完成。");
}
//get set基础方法
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Skill getSkill() {
return skill;
}
public void setSkill(Skill skill) {
this.skill = skill;
}
}
//这里是技能接口
public interface Skill {
void use(); // 释放技能的抽象方法
}
//这里是测试类主程序
public class DemoGame {
public static void main(String[] args) {
Hero hero = new Hero(); //创建 英雄对象
hero.setName("艾希"); // 设置英雄的名称
// 设置英雄技能
// hero.setSkill(new SkillImpl()); // 使用单独定义的实现类
//可以看到上面的skillimpl只是为了实现接口skill,就使用了一次,所以可以给他改成匿名内部类
// 匿名内部类写法:
// Skill skill = new Skill() {
// @Override //这里是在匿名内部类里重写了技能的使用方法
// public void use() {
// System.out.println("Pia~pia~pia~");
// }
// };
// hero.setSkill(skill);
//可以看到上面的匿名内部类创建的skill对象也只使用了一次,所以可以进一步简化:
// 同时使用匿名内部类和匿名对象:
hero.setSkill(new Skill() {
@Override
public void use() {
System.out.println("Biu~Pia~Biu~Pia~");
}
});
hero.attack();//调用展示方法
}
}
接口作为方法的参数和返回值
使用接口也可以作为方法的参数和返回值,比如ArrayList集合,正是java.util.List接口的实现类,所以,List接口作为参数或者返回值类型时,可以将 ArrayList的对象进行传递或返回。
代码示例:
public class DemoInterface {
public static void main(String[] args) {
// 左边是接口名称,右边是实现类名称,这就是多态写法
List<String> list = new ArrayList<>();
//这里调用了addnames方法,同时使用list接口作为了返回值。
List<String> result = addNames(list); //把刚刚方法和返回值传递给接口,并创建对象result
for (int i = 0; i < result.size(); i++) {
System.out.println(result.get(i));
}
}
//这里创建了一个方法,以list(字符串类型)接口作为返回参数类型,添加了几个字符串到list接口中。
public static List<String> addNames(List<String> list) {
list.add("迪丽热巴");
list.add("古力娜扎");
list.add("玛尔扎哈");
list.add("沙扬娜拉");
return list; //返回了添加好字符串的集合 这里是以list接口作为返回值返回
}
}
接口(list)作为参数时,传递它的子类对象。
接口作为返回值类型时,返回它的子类对象。
发红包案例
场景说明: 红包发出去之后,所有人都有红包,大家抢完了之后,最后一个红包给群主自己。 大多数代码都是现成的,我们需要做的就是填空题。 我们自己要做的事情有: 1. 设置一下程序的标题,通过构造方法的字符串参数 2. 设置群主名称 3. 设置分发策略:平均,还是随机? 红包分发的策略: 1. 普通红包(平均):totalMoney / totalCount,余数放在最后一个红包当中。 2. 手气红包(随机):最少1分钱,最多不超过平均数的2倍。应该越发越少。
已经写好的类:
1. RedPacketFrame :一个抽象类,包含了一些属性,是红包案例的页面。
public abstract class RedPacketFrame extends JFrame {
/*
ownerName : 群主名称 */
public String ownerName = "谁谁谁谁";
/* openMode : 红包的类型 [普通红包/手气红包]
public OpenMode openMode = null;
*/
/**
* 构造方法:生成红包界面.
*
* @param title 页面的标题.
*/
public RedPacketFrame(String title) {
super(title);
init();// 页面相关的初始化操作
}
/* set方法 */
public void setOwnerName(String ownerName) {
this.ownerName = ownerName;
}
public void setOpenMode(OpenMode openMode) {
this.openMode = openMode;
}
}
2.OpenMode :一个接口,包含一个分配方法,用来指定红包类型。
public interface OpenMode {
/**
* @param totalMoney 总金额,单位是"分"。总金额为方便计算,已经转换为整数,单位为分。
* @param count 红包个数
* @return ArrayList<Integer> 元素为各个红包的金额值,所有元素的值累和等于总金额.
*
* 请将totalMoney,分成count分,保存到ArrayList<Integer>中,返回即可.
*/
public abstract ArrayList<Integer> divide(int totalMoney, int count);
}
首先我们先创建一个子类myred,继承RedPacketFrame抽象类,并重写他的抽象方法:
public class MyRed extends RedPacketFrame {
/**
* 构造方法:生成红包界面。
*
* @param title 界面的标题
*/
public MyRed(String title) {
super(title);
}
}
然后创建一个测试类,并创建myred的对象。
然后我们需要重写平均分发与随机分发的方法,首先创建平均分发与随机分发的实现类,继承OpenMode接口,实现里面的抽象方法。
然后再在实现类中创建相应的对象,并调用发红包方法。
public class Bootstrap {
public static void main(String[] args) {
MyRed red = new MyRed("标题"); //这里创建了一个对象,并设置左上角的标题
// 设置群主名称
red.setOwnerName("王思聪");
// 普通红包
// OpenMode normal = new NormalMode();
// red.setOpenWay(normal);
// 手气红包
OpenMode random = new RandomMode();
red.setOpenWay(random);
}
}
import java.util.ArrayList;
//创建一个平均分发实现类,并继承OpenMode接口
public class NormalMode implements OpenMode {
@Override //重写他的抽象方法
public ArrayList<Integer> divide(final int totalMoney, final int totalCount) {
ArrayList<Integer> list = new ArrayList<>(); //创建一个数组对象叫list
// totalMoney 塞多少钱 totalCount //分多少份
int avg = totalMoney / totalCount; // 平均值 这里用总金额/人数=每个人的红包塞多少钱
int mod = totalMoney % totalCount; // 余数,模,零头
//然后通过除余,求出剩下多少没除开的,把他塞到最后一个红包中。
// 注意totalCount - 1代表,最后一个先留着
//循环人数-1次,比如有三个人就循环两次,每次循环放一个钱数进去。
for (int i = 0; i < totalCount - 1; i++) {
list.add(avg);
}
// 有零头,需要放在最后一个红包当中
list.add(avg + mod); //最后一次除了固定的平均值还把余数放进去了
return list; //返回这个生成的数组。
}
}
//这里继承了发红包的抽象类,重写了他的抽象方法
public class RandomMode implements OpenMode {
@Override
public ArrayList<Integer> divide(final int totalMoney, final int totalCount) {
ArrayList<Integer> list = new ArrayList<>(); //创建了一个数组对象
// 随机分配,有可能多,有可能少。
// 最少1分钱,最多不超过“剩下金额平均数的2倍”
// 第一次发红包,随机范围是0.01元~6.66元
// 第一次发完之后,剩下的至少是3.34元。
// 此时还需要再发2个红包
// 此时的再发范围应该是0.01元~3.34元(取不到右边,剩下0.01)
// 总结一下,范围的【公式】是:1 + random.nextInt(leftMoney / leftCount * 2);
Random r = new Random(); // 首先创建一个随机数生成器
// totalMoney是总金额,totalCount是总份数,不变
// 额外定义两个变量,分别代表剩下多少钱,剩下多少份
int leftMoney = totalMoney; //目前还剩多少钱
int leftCount = totalCount; //目前还剩多少份
// 随机发前n-1个,最后一个不需要随机
for (int i = 0; i < totalCount - 1; i++) {
// 按照公式生成随机金额
int money = r.nextInt(leftMoney / leftCount * 2) + 1;
//注:random.nextInt()的方法是指生成从0-A之间的随机数,所以这里用0~到还剩多少钱/剩余人数*2
//+1是为了确保他至少有一分钱,不是0!
list.add(money); // 将一个随机红包放入集合
leftMoney -= money; // 剩下的金额越发越少
leftCount--; // 剩下还应该再发的红包个数,递减
}
// 最后一个红包不需要随机,直接放进去就得了
list.add(leftMoney);
return list;
}
}