查看笔记前,建议您先查阅:笔记说明

Java Note two


day10 字符串

String.format()

开幕雷击

String.format比\t好用多了

printf可以直接输出(不是println),不用String.format;要将字符串fomat后给String类型变量再用。

// 直接输出,没问题的,但要记得换行!!!
System.out.printf("%10s%10s%10s%10s", "ID", "姓名", "年龄", "地址");
标志 说明 示例 结果
+ 为正数或者负数添加符号 (“%+d”,15) +15
- 左对齐(不写则默认右对齐) ("%-5d", 15) |15 |(竖线仅表现用)
0 数字前面补0(加密常用) (“%04d”, 99) 0099
空格 整数之前添加指定数量的空格 (“% 4d”, 99) 99
, 以“,”对数字分组(常用显示金额) (“%,f”, 9999.99) 9,999.990000
( 使用括号包含负数 (“%(f”, -99.99) (99.990000)
# 如果是浮点数则包含小数点,如果是16进制或8进制则添加0x或0 (“%#x”, 99)(“%#o”, 99) 0x63 0143
< 格式化前一个转换符所描述的参数 (“%f和%<3.2f”, 99.45) 99.450000和99.45

代码测试:

// 不足10的会在前面补足10个空格
// 足10的按正常输入
// 一个方法里可以有多个参数,记得多%s

String s = "Hello World";   // 11
String s1 = "1234";   // 4
String format = String.format("%10s%10s", s, s1);   
System.out.println(format.length());   // 21
//Hello World      1234
System.out.println(format);

数字处理:

// 如果% d里的数字位数大于数字,则左补充空格
// 否则,就一个空格
//     123456
String format1 = String.format("% 10d", 123456); 
System.out.println(format1);
System.out.println(format1.length());  // 10
  • 字符串不可变,它们的值在创建后不能被更改。赋值其实是产生了一个新的字符串。
  • 虽然 String 的值是不可变的,但是它们可以被共享
  • 字符串效果上相当于字符数组( char[] ),但是底层原理是字节数组( byte[] )

String类的构造方法

  • 常用的构造方法
    方法名 说明
    public String() 创建一个空白字符串对象,不含有任何内容
    public String(char[] chs) 根据字符数组的内容,来创建字符串对象
    public String(byte[] bys) 根据字节数组的内容,来创建字符串对象
    String s = “abc”; 直接赋值的方式创建字符串对象,内容就是abc
  • 代码示例:
String s = new String();
System.out.println(s);   // 输出啥都没有

char[] chars = {'a', 'b', 'c'};
String ss = new String(chars);  // abc
System.out.println(ss);

char[] char1 = {97, 110, 65};
String ssr = new String(char1);  // anA
System.out.println(ssr);

byte[] bytes = {65, 66, 68};
String sss = new String(bytes);
System.out.println(sss);  // ABD

创建字符串对象两种方式的区别

  • 通过构造方法创建

    通过 new 创建的字符串对象,每一次 new 都会申请一个内存空间,虽然内容相同,但是地址值不同

  • 直接赋值方式创建

    “” 方式给出的字符串,只要字符序列相同(顺序和大小写),无论在程序代码中出现几次,JVM 都只会建立一个 String 对象,并在字符串池中维护

  • 比较字符串

    一般使用 "XXX".equals(变量),避免空指针异常。

// 比较字符串 .equals(String)
//==号比较基本数据类型:比较的是具体的值。比较引用数据类型:比较的是对象地址值
char[] chars = {'a', 'b', 'c'};
String s1 = new String(chars);
String s2 = new String(chars);

System.out.println(s1 == s2);  // false

String s3 = "abc";
String s4 = "abc";
System.out.println(s3 == s4); // trye

System.out.println(s1 == s3);  // false
  • 直接赋值的字符串变量,将变量赋值给另外一个变量,赋的不是地址。
String s1 = "abc";
String s2 = s1;

System.out.println(s2);  // abc

s1 = "666";
System.out.println(s1);   // 666
System.out.println(s2);   // abc

面试水题

public class StringTest {
    public static void main(String[] args) {
        String s1 = "abc";  // 记录串池中的地址值
        String s2 = "ab";
        String s3 = s2 + "c"; // new 出来的
        System.out.println(s1 == s3);  // false
    }
}

字符串拼接的时候,如果有变量:
JDK8以前:系统底层会自动创建一个StringBuilder对象,然后再调用其append方法完成拼接。
拼接后,再调用其toString方法转换为String类型,而toString方法的底层是直接new了一个字符串对象。
JDK8版本:系统会预估要字符串拼接之后的总大小,把要拼接的内容都放在数组中,此时也是产生一个新的字符串。

String ss1 = "abc";
String ss2 = "a" + "b" + "c";  // 拼接无变量
System.out.println(ss1 == ss2);  // true

在编译的时候,就会将"a" + "b" + "c" 拼接为"abc"。

用户输入的字符串与直接赋值的字符串,地址不同:

Scanner底层是new出来的

Scanner sc = new Scanner(System.in);
String next = sc.next();
System.out.println(next);

String s = "abc";
System.out.println(s == next); // false

String内存

StringTable在JDK7开始从方法区中挪到了堆内存。

直接赋值:

String内存

  • 所以,直接赋值的方式,代码简单,而且节约内存。

new出来的

String2


一些方法

string.length();
toCharArray();
charAt();
// charAt使用时,判断一个数是大写,小写还是数字:
// 使用charAt加length()时
// 数字一定要加 ‘’ 单引号

if (c >= 'a' && c <= 'z') 
// char类型变量比较时,自动提升int查询。ASCII
if (c >= 'A' && c <= 'Z') {
else if (c >= '0' && c <= '9') {

// 实现手机号码中间屏蔽
subString()
replace()

spilt()
// 统计字符串某个字符,不可用该方法,因为如果出现在最后则GG
// 字符串前面有一个空格则会输出一个[]空,两个则生成两个空字符串
// 中间有一个空格则直接被spilt拿走不显示 空。
// 中间有两个空格则显示一个[]空字符串,三个空格显示两个。
// 结尾有多少个空格都不显示[]。
String str = " 前面一个  前面两个   前面三个 后面两个  ";
String[] s = str.split(" ");
StringJoiner sj = new StringJoiner("], [", "[", "]。END");
for(String ss: s) {
     sj.add(ss);
}
System.out.println(sj);
System.out.println(s.length);
//  ===============================================
// [], [前面一个], [], [前面两个], [], [], [前面三个], [后面两个]。END
// 8

indexOf(String str)
indexOf(String str, int fromIndex)

记一个计字符串出现次数的大佬思路

现有如下文本:"Java语言是面向对象的,Java语言是健壮的,Java语言是安全的,Java是高性能的,Java语言是跨平台的"。请编写程序,统计该文本中"Java"一词出现的次数。

我自己的思路:while true配合indexOf(String str, int fromIndex)进行++计数。

但,更牛逼的方法是,用replace方法,将替换后的数组长度差异 去除于 "Java"->4

public class Test {
    public static void main(String[] args) {
        String str = "Java语言是面向对象的,Java语言是健壮的,Java语言是安全的,Java是高性能的,Java语言是跨平台的";
        String tar = "Java";
        // 调用方法并输出
        System.out.println(search(str, tar));

    }

    // 替换之后求长度差
    public static int search(String str, String tar) {
        String newStr = str.replace(tar, "");
        int count = (str.length() - newStr.length()) / tar.length();
        return count;

    }
}

StringBuilder

  • StringBuilder 可以看成是一个容器,创建之后里面的内容是可变的。
  • 当我们在拼接字符串和反转字符串的时候会使用到。
  • 支持链式编程
// StringBuilder有参构造,容器默认带值。
StringBuilder sb = new StringBuilder("abc");

// 打印对象不是地址值而是属性值。
sout(sb);

StringJoiner

  • StringJoiner跟StringBuilder一样,也可以看成是一个容器,创建之后里面的内容是可变的。
  • 作用:提高字符串的操作效率,而且代码编写特别简洁,但是目前市场上很少有人用。
  • JDK8出现的

经典例题:数组为int[] arr = {1,2,3}; 执行方法后的输出结果为:[1, 2, 3]

Stringjoiner


构造方法

方法名 说明
public StringJoiner (间隔符号) 创建一个StringJoiner对象,指定拼接时的间隔符号
public StringJoiner(间隔符号,开始符号,结束符号) 创建一个StringJoiner对象,指定拼接时的间隔符号、开始符号、结束符号

成员方法

方法名 说明
public StringJoiner add (添加的内容) 添加数据,并返回对象本身直接add数字不行,还要 +"" 变成字符串
public int length() 返回长度(字符出现的个数)包括添加后的各种符号(开始,结束,间隔)
public String toString() 返回一个字符串 (该字符串就是拼接之后的结果)

关于字符串的小扩展

  • 部分内容可以看前面的面试水题
  • 拼接时没有变量:

底层无变量

  • JDK8前 拼接时有变量:

JDK8之前,一次+号,新增两个对象,StringStringBuilder

String变量加

  • JDK8拼接时有对象:

预估并创建数组。假如和上面那样多行,则还是浪费时间。

预估

字符串拼接的时候有变量参与:在内存中创建了很多对象浪费空间,时间也非常慢。

结论:
如果很多字符串变量拼接,不要直接+。在底层会创建多个对象,浪费时间,浪费性能。

StringBuilder底层提高效率原理

  • 所有要拼接的内容都会往StringBuilder中放,不会创建很多无用的空间,节约内存

StringBuilder源码分析

  • 默认创建一个长度为16的字节数组
  • 添加的内容长度小于16,直接存
  • 添加的内容大于16会扩容(原来的容量*2+2)
  • 如果扩容之后还不够,以实际长度为准
        StringBuilder sb = new StringBuilder();
        // 容量: 最多装多少
        // 长度: 已经装了多少
        System.out.println(sb.capacity()); // 16
        System.out.println(sb.length());  // 0

        sb.append("abc");

        System.out.println(sb.capacity()); // 16
        System.out.println(sb.length());  // 3

//        sb.append("abcdefghijklmnopqrstuvwxyz");
        sb.append("abcdefghijklmnopqrstuvwxyz1234568790102");

//        System.out.println(sb.capacity()); // 34 (16 * 2 + 2)
//        System.out.println(sb.length());  // 29
        System.out.println(sb.capacity()); // 42
        System.out.println(sb.length());  // 42

day11 集合ArrayList

集合和数组的优势对比:

  1. 长度可变
  2. 添加数据的时候不需要考虑索引,默认将数据添加到末尾

1.1 ArrayList类概述

  • 什么是集合

    提供一种存储空间可变的存储模型,存储的数据容量可以发生改变

  • ArrayList集合的特点

    长度可以变化,只能存储 引用 数据类型。(包装类)

  • 泛型的使用

    用于约束集合中存储元素的数据类型

    ArrayList


1.1.1 基本类型对应的包装类

基本类型 包装类(基本就是大写)
byte Byte
short Short
char Character
int Integer
long Long
float Float
double Double
boolean Boolean

1.2 ArrayList类常用方法

1.2.1 构造方法

方法名 说明
public ArrayList() 创建一个空的集合对象
ArrayList<String> list = new ArrayList<>();

1.2.2 成员方法

方法名 说明
public boolean add(要添加的元素) 将指定的元素追加到此集合的末尾
public boolean remove(要删除的元素) 删除指定元素,返回值表示是否删除成功
public E remove(int index) 删除指定索引处的元素,返回被删除的元素
public E set(int index,E element) 修改指定索引处的元素,返回被修改的元素
public E get(int index) 返回指定索引处的元素
public int size() 返回集合中的元素的个数
  • list用for i遍历索引判断要不要删除时,如果删除,记得索引--一旦删除元素,后面的元素会往前走,索引再加1就会有元素遗漏,所以删除后要--。

1.3 list.forr遍历

快捷键:

  • list.fori 正向遍历
  • list.forr 倒着遍历

list.add(null) 也会输出null


1.4 list的存入问题

list存入的是引用对象的地址。所以下图,如果创建学生对象在循环外,因为list每次存入的都是循环外new出来对象的那个地址值,所以存入的对象都是同一个。

集合存对象

  • ArrayList也可作为结果放回。(当我们方法要返回多个数据时)
private static ArrayList<Phone> showLowPrice(int price, ArrayList<Phone> list)

1.5 list搞验证码

生成一个验证码,5位数,由数字,大写字母,小写字母组成。

可以定义个Character集合,将所有内容存入,有随机索引生成。

如果指定数字位数,可以将数字并入的 前面生成的随机字母 后面,通过toCharArray转char数组,将后面数字随机调换位置。

// 加入字母示例
for (int i = 0; i < 26; i++) {
    list.add((char) ('a' + i));
    list.add((char) ('A' + i));
}

day13面向对象进阶

1.1 static

在student类中,如果我没想要增加一个属性“语文教师的名字”teacherName,在创建学生对象时,每次都要赋同样的值。而如果在Student类中,将该属性定位static, 则一次赋值,次次有值。

在javabean类中成员变量直接String teacherName = .....也行,但推荐加static。

s1.teacherName= "George";
// or
Student.teacherName = "George";

1.1.1 定义格式和使用

static是静态的意思。 static可以修饰成员变量或者修饰方法。

静态方法 中 只能调用静态方法 和 静态变量静态方法 中 只能调用静态方法 和 静态变量

1.1.2 静态变量及其访问

有static修饰成员变量,说明这个成员变量是属于类的,这个成员变量称为类变量或者静态成员变量。 直接用 类名访问即可。因为类只有一个,所以静态成员变量在内存区域中也只存在一份。所有的对象都可以共享这个变量。

定义格式

修饰符 static 数据类型 变量名 = 初始值;    

举例

public class Student {
    public static String schoolName = "Alibaba"; // 属于类,只有一份。
    // .....
}

静态成员变量的访问:

格式:类名.静态变量

对象名调用(不推荐)

public static void  main(String[] args){
    System.out.println(Student.schoolName); // Alibaba
    Student.schoolName = "Tencent";
    System.out.println(Student.schoolName); // Tencent
}

1.1.3 实例变量及其访问

无static修饰的成员变量属于每个对象的, 这个成员变量叫实例变量,之前我们写成员变量就是实例成员变量。

需要注意的是:实例成员变量属于每个对象,必须创建类的对象才可以访问。

格式:对象.实例成员变量


1.1.4 静态方法及其访问

一、多用于测试类和工具类中

1.1.4.1 工具类

帮助我们做一些事情的,但是不描述任何事物的类。

(1)类名见名知意

(2)私有化构造方法(外界不能创建该类的对象)

(3)方法定义为静态,类名直接调用

public class ArrUtil{
    private ArrUtil(){}

    public static int getMax(...){...}
    public static int getMin(...){...}
    public static int getAvg(...){...}
    public static int getSum(...){...}
}

(4)调用,工具类名.方法

System.out.println(ArrayUtil.getAerage(arr1));

二、javabean中很少用(设计模式)

有static修饰成员方法,说明这个成员方法是属于类的,这个成员方法称为类方法或者静态方法**。 直接用 类名访问即可。因为类只有一个,所以静态方法在内存区域中也只存在一份。所有的对象都可以共享这个方法。

与静态成员变量一样,静态方法也是直接通过类名.方法名称即可访问。

举例

public class Student{
    public static String schoolName = "Alibaba"; // 属于类,只有一份。
    // .....
    public static void study(){
        System.out.println("我们都在阿里巴巴学习");   
    }
}

静态成员变量的访问:

格式:类名.静态方法

public static void  main(String[] args){
    Student.study();
}

1.1.5 实例方法及其访问

无static修饰的成员方法属于每个对象的,这个成员方法也叫做实例方法

需要注意的是:实例方法是属于每个对象,必须创建类的对象才可以访问。

格式:对象.实例方法

1.1.6 static内存图

static内存


1.2 static小结

1.当 static 修饰成员变量或者成员方法时,该变量称为静态变量,该方法称为静态方法。该类的每个对象都共享同一个类的静态变量和静态方法。任何对象都可以更改该静态变量的值或者访问静态方法。但是不推荐这种方式去访问。因为静态变量或者静态方法直接通过类名访问即可,完全没有必要用对象去访问。

2.无static修饰的成员变量或者成员方法,称为实例变量,实例方法,实例变量和实例方法必须创建类的对象,然后通过对象来访问。

3.static修饰的成员属于类,会存储在静态区,是随着类的加载而加载的,且只加载一次,所以只有一份,节省内存。存储于一块固定的内存区域(静态区),所以,可以直接被类名调用。它优先于对象存在,所以,可以被所有对象共享。

4.无static修饰的成员,是属于对象,对象有多少个,他们就会出现多少份。所以必须由对象调用。


1.3 继承(面向对象三大特征之一)

(封装:对象代表什么,就得封装对应的数据,并提供数据对应的行为)

解决多个类定义重复的属性与重复的行为方法。(学生老师都有姓名年龄,都要吃饭睡觉)。将相同属性和行为抽取到单独一个类中,子类特有的属性和行为由自己定义,那么多个类无需再定义这些属性和行为,只要继承那一个类即可。

其中,多个类可以称为子类(派生类),单独被继承的那一个类称为父类超类(superclass)或者基类

  • 每一个类都直接或者间接的继承于Object类

1.3.1 继承的含义

继承描述的是事物之间的所属关系,这种关系是:is-a(英语:subsumption,包含架构) 的关系。例如,兔子属于食草动物,食草动物属于动物。可见,父类更通用,子类更具体。我们通过继承,可以使多种事物之间形成一种关系体系。

继承:就是子类继承父类的属性行为,使得子类对象可以直接具有与父类相同的属性、相同的行为。子类可以直接访问父类中的非私有的属性和行为。

父类的方法如果private了,子类就无法使用。(如果父类不写权限修饰符,则默认package-private)

当类与类之间,存在相同(共性)的内容,并满足子类是父类的一种,就可以考虑继承。


1.3.2 继承的好处

  1. 提高代码的复用性(减少代码冗余,相同代码重复利用)。
  2. 使类与类之间产生了关系。
  3. 子类可以在父类的基础上,增加其他的功能,是子类更强大。

1.3.3 继承的格式

通过 extends 关键字,可以声明一个子类继承另外一个父类,定义格式如下:

class 父类 {
    ...
}

class 子类 extends 父类 {
    ...
}

需要注意:Java是单继承的,一个类只能继承一个直接父类,跟现实世界很像,但是Java中的子类是更加强大的。(支持单继承,不支持多继承,但支持多层继承(父类继承其他类,爷爷是孙子的间接父类))

继承中,如果子类要拓展属性,那父类写有参构造方法和toString方法感觉是很奇怪的。

1.为什么在实例化子类的对象的时候会调用先调用父类的构造方法?

答:因为子类继承父类之后,获取到了父类的内容(属性/字段),而这些内容在使用之前必须先初始化,所以必须先调用父类的构造函数进行内容的初始化.


子类一定要实现父类的有参构造方法,

你在Xx类里定义了一个带参数的构造方法,那么这个Xx类就没有无参数的构造方法了

子类在继承父类时,如果没有相同的带参构造方法,那么他就需要在其构造方法中明确的通过super()调用父类的带参构造方法,否则构造不出父类,从而也构造不出他自己了。

你如果在父类中写个不带参数的构造方法,就可以不用实现父类的带参构造方法了。


1.4 子类不能继承的内容

son

子类不能继承父类的构造方法。(构造方法名也对不上)

值得注意的是子类可以继承父类的私有成员(成员变量,方法?),只是子类无法直接访问而已,可以通过getter/setter方法访问父类的private成员变量。

1.4.1 继承内存图

方法区加载子类的同时也会加载父类,堆内存中也会有两部分,一部分存父类继承的,一部分空间存自己子类的成员变量。

非private:

继承内存

private修饰,则在执行到z.name和z.age的时候赋值失败,代码报错。


1.4.2 成员方法是否可以被继承

虚方法

非private,非static,非final.继承的时候会将表交给子类,提高性能,方法重写。

只有父类中的虚方法才能被子类继承。

虚方法

内存图:

私有方法


1.5 继承后的特点——成员变量

就近原则:谁离我近,我就用谁。(先在局部位置找,本类成员位置找,父类成员位置找,逐级往上)

1.5.1 成员变量不重名

如果子类父类中出现不重名的成员变量,这时的访问是没有影响的

1.5.2 成员变量重名

如果子类父类中出现重名的成员变量,这时的访问是有影响的。代码如下:

class Fu1 {
    // Fu中的成员变量。
    int num = 5;
}
class Zi1 extends Fu1 {
    // Zi中的成员变量
    int num = 6;

    public void show() {
        // 访问父类中的num
        System.out.println("Fu num=" + num);
        // 访问子类中的num
        System.out.println("Zi num=" + num);
    }
}
class Demo04 {
    public static void main(String[] args) {
        // 创建子类对象
        Zi1 z = new Zi1(); 
        // 调用子类中的show方法
        z1.show(); 
    }
}
演示结果:
Fu num = 6
Zi num = 6

子父类中出现了同名的成员变量时,子类会优先访问自己对象中的成员变量。如果此时想访问父类成员变量如何解决呢?我们可以使用super关键字。

1.5.3 super访问父类成员变量

子父类中出现了同名的成员变量时,在子类中需要访问父类中非私有成员变量时,需要使用super 关键字,修饰父类成员变量,类似于之前学过的 this 。(没有super.super)

需要注意的是:super代表的是父类对象的引用,this代表的是当前对象的引用。

class Fu {
    // Fu中的成员变量。
    int num = 5;
}

class Zi extends Fu {
    // Zi中的成员变量
    int num = 6;

    public void show() {
        int num = 1;

        // 访问方法中的num
        System.out.println("method num=" + num);
        // 访问子类中的num
        System.out.println("Zi num=" + this.num);
        // 访问父类中的num
        System.out.println("Fu num=" + super.num);
    }
}

class Demo04 {
    public static void main(String[] args) {
        // 创建子类对象
        Zi1 z = new Zi1(); 
        // 调用子类中的show方法
        z1.show(); 
    }
}

演示结果:
method num=1
Zi num=6
Fu num=5

小贴士:Fu 类中的成员变量是非私有的,子类中可以直接访问。若Fu 类中的成员变量私有了,子类是不能直接访问的。通常编码时,我们遵循封装的原则,使用private修饰成员变量,那么如何访问父类的私有成员变量呢?对!可以在父类中提供公共的getXxx方法和setXxx方法。

如果变量只在父类出现,则以下三行代码一样:

System.out.println(hobby);// 局部变量找不到,本类位置找,再去父类找。
System.out.println(this.hobby);// 先从本类成员位置找,找不到去父类找
System.out.println(super.hobby); // 直接去父类成员位置找

1.6 继承后的特点——成员方法

就近原则。

1.6.1 成员方法不重名

如果子类父类中出现不重名的成员方法,这时的调用是没有影响的

1.6.2 成员方法重名

如果子类父类中出现重名的成员方法,则创建子类对象调用该方法的时候,子类对象会优先调用自己的方法。(重写override)(当然也可以用super.调用父类方法)


1.7 方法重写

方法重写 :子类中出现与父类一模一样的方法时(返回值类型,方法名和参数列表都相同),会出现覆盖效果,也称为重写或者复写。声明不变,重新实现。本质是覆盖虚方法表中的方法。

1.7.1 @Override重写注解

  • @Override:注解,重写注解校验!
  • 这个注解标记的方法,就说明这个方法必须是重写父类的方法,否则编译阶段报错。
  • 建议重写都加上这个注解,一方面可以提高代码的可读性,一方面可以防止重写出错!

1.7.2 注意事项

  1. 方法重写是发生在子父类之间的关系。
  2. 子类方法覆盖父类方法,必须要保证权限大于等于父类权限。(空着不写<protected<public)
  3. 子类方法覆盖父类方法,返回值类型、函数名和参数列表都要一模一样
  4. 子类重写父类方法是,返回值类型子类必须小于等于父类。

网友观点:子类重写父类方法,假设子类和父类都有抛出异常的逻辑,假设父类抛出了IOException,而子类抛出了Exception(包含IOException),你可以想象一张小渔网怎么能网住一头鲸鱼,这就是子类抛出异常必须是父类异常的派生类的原因。
所以从返回上说,可以推出子类方法返回值类型不能大于父类方法,因为返回时抛出的异常必须能被父类捕获。
再补充一点:为什么重写还有一条规则是访问控制符必须高于父类?那是因为为了多态的实现。有时候父类需要上转型引用子类的方法实现多态,这就导致子类方法的访问控制符必须高于父类,否则子类里写成了private,父类还怎么访问?
同理,子类方法返回值类型小于父类,因为父类引用子类方法实现多态,你不能让一个返回空的方法的返回值经过一个不能返回空的方法出去。

子类方法返回Dog,父类方法返回Animal(Dog类继承Animal类),则没问题。

  1. 只有被添加到虚方法表中方法才能被重写。(私有方法不能被重写,子类不能重写父类的静态方法,否则会报错。)

1.8 继承后的特点—构造方法

  1. 构造方法的名字是与类名一致的。所以子类是无法继承父类构造方法的。
  2. 构造方法的作用是初始化对象成员变量数据的。所以子类的初始化过程中,必须先执行父类的初始化动作。子类的构造方法中默认有一个super() ,表示调用父类的构造方法,父类成员变量初始化后,才可以给子类使用。(先有爸爸,才能有儿子

继承后子类构方法器特点:子类所有构造方法的第一行都会默认先调用父类的无参构造方法,再执行自己。

调用构造方法格式:

super(...) -- 调用父类的构造方法,根据参数匹配确认
this(...) -- 调用本类的其他构造方法,根据参数匹配确认
class Person {
    private String name ="凤姐";
    private int age = 20;

    public Person() {
        System.out.println("父类无参");
    }

    public Person(String name , int age){
        this.name = name ;
        this.age = age ;
    }

    // getter/setter省略
}

class Student extends Person {
    private double score = 100;

    public Student() {
        //super(); // 调用父类无参构造方法,默认就存在,可以不写,必须再第一行
        System.out.println("子类无参");
    }
    // ===============================================================
     public Student(String name , int age, double score) {
    // ===============================================================
         // 借助与super(...)去调用父类构造方法,以便初始化继承自父类对象的name和age.
         // 调用父类有参构造方法Person(String name , int age)初始化name和age
        super(name ,age);
        this.score = score;    
        System.out.println("子类有参");
     }
      // getter/setter省略
}

public class Demo07 {
    public static void main(String[] args) {
        // 调用子类有参数构造方法
        Student s2 = new Student("张三",20,99);
        System.out.println(s2.getScore()); // 99
        System.out.println(s2.getName()); // 输出 张三
        System.out.println(s2.getAge()); // 输出 20
    }
}
  • 带全部参数的构造,用super将共有的属性传递给父类进行赋值,子类特有的属性在下一行开始赋值this.XXX = XXX; (IDEA Alt+insert选择构造方法,全选后快速生成)

1.9 super与this

1.9.1 this

this可以理解为局部变量,view -> showBytecode

this

// 可以吧对象在内存中的结构打印出来
ClassLayout layout = ClassLayout.parseInstance(s);
System.out.println(layout.toPrintable())
  • 默认是去找本类中的其他构造方法,根据参数来确定具体调用哪一个构造方法。
  • 通过this调用本类其他构造方法,通常给数据赋默认值时使用。

this调用本类

1.9.2 super

super:代表父类存储空间。

在每次创建子类对象时,先初始化父类空间,再创建其子类对象本身。目的在于子类对象中包含了其对应的父类空间,便可以包含其父类的成员,如果父类成员非private修饰,则子类可以随意使用父类成员。代码体现在子类的构造七调用时,一定先调用父类的构造方法。理解图解如下:

super

小结:

  • 子类构造方法执行的时候,都会在第一行默认先调用父类无参数构造方法一次。
  • 子类构造方法的第一行都隐含了一个super()去调用父类无参数构造方法,super()可以省略不写。
  • 子类的每个构造方法中均有默认的super(),调用父类的空参构造。手动调用父类构造会覆盖默认的super()。
  • super() 和 this() 都必须是在构造方法的第一行,所以不能同时出现。
  • super(..)和this(...)是根据参数去确定调用父类哪个构造方法的。
  • super(..)可以调用父类构造方法初始化继承自父类的成员变量的数据。
  • this(..)可以调用本类中的其他构造方法。

day14多态&包&final&权限修饰符&代码块


1. 多态

1.1 多态的形式

  • 多态是继封装、继承之后,面向对象的第三大特性。
  • 多态是出现在继承或者实现关系中的
  • 多态:同类型的对象,表现出的不同形态。

多态体现的格式

父类类型 变量名 = new 子类/实现类构造器;
变量名.方法名();

多态的前提:有继承关系,子类对象是可以赋值给父类类型的变量。例如Animal是一个动物类型,而Cat是一个猫类型。Cat继承了Animal,Cat对象也是Animal类型,自然可以赋值给父类类型的变量。

1.2 多态的使用场景

如果没有多态,在下图中register方法只能传递学生对象,其他的Teacher和administrator对象是无法传递给register方法方法的,在这种情况下,只能定义三个不同的register方法分别接收学生,老师和管理员。

多态的应用场景

有了多态之后,方法的形参就可以定义为共同的父类Person。

要注意的是:

  • 当一个方法的形参是一个类,我们可以传递这个类所有的子类对象。
  • 当一个方法的形参是一个接口,我们可以传递这个接口所有的实现类对象(后面会学)。
  • 而且多态还可以根据传递的不同对象来调用不同类中的方法。

多态的应用场景2

1.3 多态的定义和前提

多态: 是指同一行为,具有多个不同表现形式。

从上面案例可以看出,Teacher和Student都是动物,都有注册这一行为,但是出现的效果.show()(表现形式)是不一样的。

前提【重点】

  1. 有继承或者实现关系
  2. 方法的重写【意义体现:不重写,无意义
  3. 父类引用指向子类对象【格式体现】

    父类类型:指子类对象继承的父类类型,或者实现的父接口类型。

1.4 多态的运行特点

调用成员变量时:编译看边(父类),运行也看

调用成员方法时:编译看左边,运行看右边(子类)

代码示例:

Fu f = new Zi();
//javac编译时看左边的父类中有没有name这个属性,没有就报错
//在实际运行的时候,把  父类name属性的值  打印出来
System.out.println(f.name);
//编译看左边的父类中有没有show这个方法,没有就报错
//在实际运行的时候,运行的是子类中的show方法
f.show();

1.4.1 理解

Animal a = new Dog();

创建对象后用a去调用变量和方法。而a是Animal类型的,所以默认都会从Animal这个类中去找。

  • 成员变量:在子类的对象中,会把父类的成员变量也继承下来的。父:name,子:name
  • 成员方法:如果子类对方法进行了重写,那么在虚方法表中是会把父类的方法进行覆盖的。

内存图解:

多态内存


1.5 多态的优势和弊端

1.5.1 多态的优势

多态的好处:

  • 使用父类型作为参数,可以接收所有子类对象,体现多态的扩展性与便利。
sb.append(Object e);
  • 在多态的形式下,右边对象可以实现解耦合,便于扩展和维护。
Person p = new Student();
p.work();  // 业务逻辑发生改变时,后续代码无需修改。

1.5.2 多态的弊端

我们已经知道多态编译阶段是看左边父类类型的,如果子类有些独有的功能,此时多态的写法就无法访问子类独有功能了

Animal a = new Cat();
 // catchMouse方法为Cat类独有,Animal没有,编译报错
// 方法编译看左边,Animal没有该方法
a.catchMouse(); 

解决方法:变回子类类型。Cat c = (Cat) a;

1.6 引用类型转换

1.6.1 为什么要转型

多态的写法就无法访问子类独有功能了。

当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误。也就是说,不能调用子类拥有,而父类没有的方法。编译都错误,更别说运行了。这也是多态给我们带来的一点"小麻烦"。所以,想要调用子类特有的方法,必须做向下转型。

回顾基本数据类型转换

  • 自动转换: 范围小的赋值给范围大的.自动完成:double d = 5;
  • 强制转换: 范围大的赋值给范围小的,强制转换:int i = (int)3.14

    多态的转型分为向上转型(自动转换)与向下转型(强制转换)两种。

1.6.2 向上转型(自动转换)

  • 向上转型:多态本身是子类类型向父类类型向上转换(自动转换)的过程,这个过程是默认的。
    当父类引用指向一个子类对象时,便是向上转型。
    使用格式:
父类类型  变量名 = new 子类类型();
如:Animal a = new Cat();

原因是:父类类型相对与子类来说是大范围的类型,Animal是动物类,是父类类型。Cat是猫类,是子类类型。Animal类型的范围当然很大,包含一切动物。所以子类范围小可以直接自动转型给父类类型的变量。

1.6.3 向下转型(强制转换)

  • 向下转型:父类类型向子类类型向下转换的过程,这个过程是强制的。
    一个已经向上转型的子类对象,将父类引用转为子类引用,可以使用强制类型转换的格式,便是向下转型。

使用格式:

子类类型 变量名 = (子类类型) 父类变量名;
如:Aniaml a = new Cat();
   Cat c =(Cat) a;  

强转成Cat类没毛病,如果强转为Dog类则会报 ClassCastException ,类型转换异常。

1.7 instanceof 关键字

  • 作用是:测试它 左边的对象是否是它右边的类的实例,返回 boolean 的数据类型 。
if (a instanceof Cat) {
    Cat c =(Cat) a;
    c.catchMouse();
} else if(a instanceof Dog) {
    Dog d = (Dog) a;
    d.watchHome();
} else{...}

变量名 instanceof 数据类型 
如果变量属于该数据类型或者其子类类型,返回true。
如果变量不属于该数据类型或者其子类类型,返回false。

1.7.1 instanceof新特性

JDK14的时候提出了新特性,把判断和强转合并成了一行

//新特性
//先判断a是否为Dog类型,如果是,则强转成Dog类型,转换之后变量名为d
//如果不是,则不强转,结果直接是false
if(a instanceof Dog d){
    d.lookHome();
}else if(a instanceof Cat c){
    c.catchMouse();
}else{
    System.out.println("没有这个类型,无法转换");
}

2. 包

2.1 包

包在操作系统中其实就是一个文件夹。包是用来分门别类的管理技术,不同的技术类放在不同的包下,方便管理和维护。

包名的命名规范

路径名.路径名.xxx.xxx
// 例如:com.itheima.oa
  • 包名一般是公司域名的倒写+包的作用,全部小写。例如com.itheima.damain。(全类名,全限定名:com.itheima.domain.Student)
  • 包名必须用”.“连接。
  • 包名的每个路径名必须是一个合法的标识符,而且不能是Java的关键字。

2.2 导包

什么时候需要导包?

情况一:在使用Java中提供的非核心包中的类时

情况二:使用自己写的其他包中的类时


什么时候不需要导包?

情况一:在使用Java核心包(java.lang,如String)中的类时

情况二:在使用自己写的同一个包中的类



2.3 使用不同包下的相同类怎么办?

假设demo1和demo2中都有一个Student该如何使用?

代码示例:

//使用全类名的形式即可。
//全类名:包名 + 类名
//拷贝全类名的快捷键:选中类名crtl + shift + alt + c 或者用鼠标点copy,再点击copy Reference
com.wahson.domain.Student s1 = new com.wahson.domain.Student();
Student s2 = new Student();

3. 权限修饰符

3.1 概述

  • 权限修饰符:是用来控制一个成员(Java成员即类中的成员:变量,方法,构造方法,初始化块,内部类)能够被访问的范围的。
  • 可以修饰成员变量,方法,构造方法,内部类。

3.2 权限能力

  • public:公共的,所有地方都可以访问。(国家财政)
  • protected:本类 ,本包,其他包中的子类都可以访问。(私生子)
  • 默认(没有修饰符):本类 ,本包可以访问。(家庭存款)

    注意:默认是空着不写,不是default

  • private:私有的,当前类可以访问。(私房钱)
    public > protected > 默认 > private

不同权限的访问能力

public protected 默认 private
同一类中
同一包中的类
不同包的子类
不同包中的无关类

可见,public具有最大权限。private则是最小权限。

编写代码时,如果没有特殊的考虑,建议这样使用权限:

  • 成员变量使用private ,隐藏细节。
  • 构造方法使用 public ,方便创建对象。
  • 成员方法使用public ,方便调用方法。
  • 特例:如果方法中的代码是抽取其他方法中共性代码,这个方法一般也私有。(ArrayList中的add方法内部grow()扩容方法)

小贴士:不加权限修饰符,就是默认权限


4. final关键字

4.1 概述

Java提供了final 关键字,表示修饰的内容不可变。

  • final: 不可改变,最终的含义。可以用于修饰类、方法和变量。
    • 类:表示该类是最终类,不能被继承
    • 方法:表示该方法是最终方法,不能被重写
    • 变量:叫做常量,只能被赋值一次。

    (基本类型:数据值不能改变;

    String字符串是不可变的private final byte[] value; <- private私有。字符串的不可变行在于底层的数组不可变,而String a = "xxx";的形式,a值只是存储对象的引用地址。你要是final String a那就不能变。

    引用类型地址值不能改变,对象内部可以改变!s.setName(), 数组Array通过索引修改)


4.2 修饰类

final修饰的类,不能被继承。(如:public final class String)

格式如下:

final class 类名 {
}

代码:

final class Fu {
}
// class Zi extends Fu {} // 报错,不能继承final的类

查询API发现像 public final class Stringpublic final class Mathpublic final class Scanner 等,很多我们学习过的类,都是被final修饰的,目的就是供我们使用,而不让我们所以改变其内容。


4.3 修饰方法

final修饰的方法,不能被重写( 如Object的public final native Class<?> getClass(); )。

哪怕你不写@Override,同名同参数类型也不行。

格式如下:

修饰符 final 返回值类型 方法名(参数列表){
    //方法体
}

代码:

class Fu2 {
    // final public竟然也行
    final public void show1() {
        System.out.println("Fu2 show1");
    }
    public void show2() {
        System.out.println("Fu2 show2");
    }
}

class Zi2 extends Fu2 {
//  @Override
//  public void show1() {
//      System.out.println("Zi2 show1");
//  }
    @Override
    public void show2() {
        System.out.println("Zi2 show2");
    }
}

4.4 修饰变量-局部变量

常量命名规范:

单个单词:全部大写

多个单词:全部大写,单词之间用下划线隔开。


局部变量——基本类型
基本类型的局部变量,被final修饰后,只能赋值一次,不能再更改(如:Math.PI)。

代码如下:

// 声明变量,使用final修饰
final int a;
// 第一次赋值 
a = 10;
// 第二次赋值
a = 20; // 报错,不可重新赋值

// 声明变量,直接赋值,使用final修饰
final int b = 10;
// 第二次赋值
b = 20; // 报错,不可重新赋值

以下写法,能否通过编译?

for (int i = 0; i < 10; i++) {
    final int c = i;
    System.out.println(c);
}

可以,因为每次循环,都是一次新的变量c。这也是需要注意的地方。


4.5 修饰变量-成员变量

成员变量涉及到初始化的问题,初始化方式有显示初始化和构造方法初始化,只能选择其中一个:

  • 显示初始化(在定义成员变量的时候立马赋值)(常用);

举例:private static final String ADD_STUDENT = "1"(static静态,属于类。静态方法 中 只能调用静态方法 和 静态变量

public class Student {
    final int num = 10;
}
  • 构造方法初始化(在构造方法中赋值一次)(不常用,了解即可)。

    注意:每个构造方法中都要赋值一次!

public class Student {
    final int num = 10;
    final int num2;

    public Student() {
        this.num2 = 20;
//     this.num2 = 20;
    }

     public Student(String name) {
        this.num2 = 20;
//     this.num2 = 20;
    }
}

被final修饰的常量名称,一般都有书写规范,所有字母都大写


5. 代码块

  • 局部代码块(方法内部,能够提前结束方法的生命周期)
  • 构造代码块
  • 静态代码块

局部代码块:

public static void main(String[] args) {
    int a;
    {
        a = 10;
        System.out.println(a);   // 10
    }
    System.out.println(a);   // 也是10
}

构造代码块:(创建对象

  1. 写在成员位置的代码块
  2. 作用:可以把多个构造方法中重复的代码抽取出来
  3. 执行时机:我们在创建本类对象的时候会先执行构造代码块再执行构造方法
  4. 逐渐淘汰,因为代码块语句会先于所有构造方法执行,实际运行时可以用this()调用或方法抽取,见下图。
public class Student {
    private String name;
    private int age;

    {
        // 构造代码块,可以将构造方法中重复语句进行抽取。
        System.out.println("开始构建对象111");
    }

    public Student() {
//        System.out.println("开始构建对象111");
    }

    public Student(String name, int age) {
//        System.out.println("开始构建对象111");
        this.name = name;
        this.age = age;
    }
}

代码块


静态代码块:(随类加载

  • 格式:static{}
  • 特点:需要通过static关键字修饰,随着类的加载而加载,并且自动触发,只执行一次。(创建两个对象也只执行一次,创建对象先执行静态代码块再执行构造代码块)
  • 使用场景:在类加载的时候,做一些数据初始化的时候使用。

像是之前写的学生管理系统,可以在运行类中初始化数据以测试。

静态代码块

上面是随着类加载而初始,只执行一次,而如果将初始化数据写在main方法里,可能会被其他类多次调用,被多次new 占用内存。

public static void main(String[] args) {
    App.main(null);
    App.main(null);
    App.main(null);
    App.main(null);
}

day15 抽象类&接口&内部类

1. 抽象类

1.1 概述

父类中的方法,被它的子类们重写,子类各自的实现都不尽相同。那么父类的方法声明和方法主体,只有声明还有意义,而方法主体没有存在的意义了(因为子类对象会调用自己重写的方法)。换句话说,父类可能知道子类应该有哪个功能,但是功能具体怎么实现父类是不清楚的(由子类自己决定),父类只需要提供一个没有方法体的定义即可,具体实现交给子类自己去实现。

顶层父类往往是抽象的(如Person),一般只创建具体如Student, Teacher对象,创建Person对象往往是没有意义的。

我们把没有方法体的方法称为抽象方法。Java语法规定,包含抽象方法的类就是抽象类

  • 抽象方法 : 没有方法体的方法。
  • 抽象类:包含抽象方法的类。

1.2 abstract使用格式

abstract是抽象的意思,用于修饰方法方法和类,修饰的方法是抽象方法,修饰的类是抽象类。

1.2.1 抽象方法

使用abstract 关键字修饰方法,该方法就成了抽象方法,抽象方法只包含一个方法名,而没有方法体。

定义格式:

修饰符 abstract 返回值类型 方法名 (参数列表);

代码举例:

public abstract void run();

1.2.2 抽象类

如果一个类包含抽象方法,那么该类必须是抽象类。

注意:抽象类不一定有抽象方法(抽象类也能有普通方法),但是有抽象方法的类必须定义成抽象类。

定义格式:

abstract class 类名字 { 

}

代码举例:

public abstract class Animal {
    public abstract void run();
}

1.2.3 抽象类的使用

要求:继承抽象类的子类必须重写父类所有的抽象方法。否则,该子类也必须声明为抽象类。

此时的方法重写,是子类对父类抽象方法的完成实现,我们将这种方法重写的操作,也叫做实现方法

1.3 抽象类的特征

抽象类的特征总结起来可以说是 有得有失

有得:抽象类得到了拥有抽象方法的能力。

有失:抽象类失去了创建对象的能力。

// 创建对象时会变成这样。
public static void main(String[] args) {
    AbstractDemo a = new AbstractDemo() {
        @Override
        public void run() {

        }
    };
}

其他成员(构造方法,实例方法,静态方法等)抽象类都是具备的。


1.4 抽象类的细节

  1. 抽象类不能创建对象,如果创建,编译无法通过而报错。只能创建其非抽象子类的对象。

    理解:假设创建了抽象类的对象,调用抽象的方法,而抽象方法没有具体的方法体,没有意义。

  2. 抽象类中,可以有构造方法(构造方法是创建对象用的,抽象类不能创建对象,要构造方法干什么?),是供子类创建对象时,初始化父类成员使用的。

    理解:子类的构造方法中,有默认的super(),需要访问父类构造方法。

    父类肯定是要抽取共有属性的,创建子类对象时,会调用super(...)为属性赋值。子类extends父类时,记得添加子类自己的空参和有参构造方法。

  3. 抽象类中,不一定包含抽象方法,但是有抽象方法的类必定是抽象类。

    理解:未包含抽象方法的抽象类,目的就是不想让调用者创建该类对象,通常用于某些特殊的类结构设计。

  4. 抽象类的子类,必须重写抽象父类中所有的抽象方法,否则子类也必须定义成抽象类,编译无法通过而报错。 (孙子类要实现 父类和爷爷类中的所有抽象方法)

    理解:假设不重写所有抽象方法,则类中可能包含抽象方法。那么创建对象后,调用抽象的方法,没有意义。

  5. 抽象类存在的意义是为了被子类继承。

    理解:抽象类中已经实现的是模板中确定的成员,抽象类不确定如何实现的定义成抽象方法,交给具体的子类去实现。

1.5 抽象类存在的意义

抽象类存在的意义是为了被子类继承,否则抽象类将毫无意义。抽象类可以强制让子类,一定要按照规定的格式进行重写。


2. 接口

2.1 概述

抽象类中可以用抽象方法,也可以有普通方法,构造方法,成员变量等。那么什么是接口呢?接口是更加彻底的抽象,JDK7之前,包括JDK7!!!接口中全部是抽象方法!!!接口同样是不能创建对象(不能实例化)的

继承是拥有所有特性,在海绵宝宝,章鱼哥,珊迪类中,游泳方法不能全部适用,如要规范游泳方法的定义,可以放在接口中,让需要实现游泳方法的类实现该接口。

接口1

接口2

2.2 定义格式

//接口的定义格式:
interface 接口名称{
    // 抽象方法
}

// 接口的声明:interface
// 接口名称:首字母大写,满足“驼峰模式”

2.3 接口成员的特点

在JDK7,包括JDK7之前,接口中的只有包含:抽象方法和常量(没有构造方法)

2.3.1.抽象方法

注意:接口中的抽象方法默认会自动加上public abstract修饰程序员无需自己手写!!
​ 按照规范:以后接口中的抽象方法建议不要写上public abstract。因为没有必要啊,默认会加上。

  • JDK7以前:接口中只能定义抽象方法
  • JDK8:接口中可以定义有方法体的方法。
  • JDK9:接口中可以定义私有方法。

2.3.2 常量

在接口中定义的成员变量默认会加上: public static final 修饰。(static,方便用接口名. 调用)也就是说在接口中定义的成员变量实际上是一个常量。这里是使用public static final修饰后,变量值就不可被修改,并且是静态化的变量可以直接用接口名访问,所以也叫常量。!!!常量必须要给初始值!!!。常量命名规范建议字母全部大写,多个单词用下划线连接。

2.3.3 案例演示

public interface InterF {
    // 抽象方法!
    //    public abstract void run();
    void run();

    //    public abstract String getName();
    String getName();

    //    public abstract int add(int a , int b);
    int add(int a , int b);

    // 它的最终写法是:
    // public static final int AGE = 12 ;
    int AGE  = 12; //常量
    String FIRST_NAME = "George";

}

sout(InterF.AGE);

2.4 基本的实现

2.4.1 实现接口的概述

类与接口的关系为实现关系,即类实现接口,该类可以称为接口的实现类,也可以称为接口的子类。实现的动作类似继承,格式相仿,只是关键字不同,实现使用 implements关键字。

2.4.2 实现接口的格式

/**接口的实现:
    在Java中接口是被实现的,实现接口的类称为实现类。
    实现类的格式:*/
class 类名 implements 接口1,接口2,接口3...{

}

接口是可以被多实现的。(继承只能单继承)

2.4.3 类实现接口的要求和意义

  1. 必须重写实现的全部接口中所有抽象方法(全部方法)
  2. 如果一个类实现了接口,但是没有重写完全部接口的全部抽象方法,这个类也必须定义成抽象类。
  3. 意义:接口体现的是一种规范,接口对实现类是一种强制性的约束,要么全部完成接口申明的功能,要么自己也定义成抽象类。这正是一种强制性的规范。

亲测:如果有两个接口IA,IB,各有方法A,B。

如果一个抽象类 implements了IA和IB,因为它是抽象类C,可以不用实现全部接口,如果仅实现了接口IA,(接口IB没有实现)

然后另外一个类D继承了抽象类C,那么类D仅用(强制)重写实现接口IB的方法B。方法A可以不用再重写。

(如果类C不抽象,那他应该实现全部接口方法,子类D继承后不用实现,但类C仅重写了一个,那么作为子类就要重写完剩下的【父债子偿】)


2.5 接口与接口的多继承

Java中,接口与接口之间是可以多继承的:也就是一个接口可以同时继承多个接口。大家一定要注意:

  • 类与接口是实现关系
  • 接口与接口是继承关系 (继承!继承!继承!可以继承多个)

接口继承接口就是把其他接口的抽象方法与本接口进行了合并。

案例演示:

public interface Abc {
    void go();
    void test();
}

/** 法律规范:接口*/
public interface Law {
    void rule();
    void test();
}

 *
 *  总结:
 *     接口与类之间是多实现的。
 *     接口与接口之间是多继承的。
 * */
 // 这里是继承,继承!可以继承多个
public interface SportMan extends Law , Abc {
    void run();
}

亲测:

// 如果有一个类实现了SportMan,则他要重写上面所有的方法(去掉重复的)
// go() test() rule() run()
// 在SportMan接口中可以重写 父类接口,如:
@Override
void rule(); 

实现类还可以在继承一个类的同时实现多个接口。

public class 类名 extends 父类 implements 接口名1, 接口名2{}

2.6 接口的细节

  1. 当两个接口中存在相同抽象方法的时候,该怎么办?

只要重写一次即可。此时重写的方法,既表示重写1接口的,也表示重写2接口的。

  1. 实现类Zi,实现了一个接口,还继承了一个Fu类。假设在接口中有一个方法,父类中也有一个相同的方法。子类如何操作呢?(如果父类中有某个方法(非抽象方法),那么子类实现接口后就不用强制重写)

处理办法一:如果父类中的方法体,能满足当前业务的需求,在子类中可以不用重写。
处理办法二:如果父类中的方法体,不能满足当前业务的需求,需要在子类中重写。

  1. 如果一个接口中,有10个抽象方法,但是我在实现类中,只需要用其中一个,该怎么办?

可以在接口跟实现类中间,新建一个中间类(适配器类)
让这个适配器类去实现接口,对接口里面的所有的方法做空重写。
让子类继承这个适配器类,想要用到哪个方法,就重写哪个方法。
因为中间类没有什么实际的意义,所以一般会把中间类定义为抽象的,不让外界创建对象


2.7 接口中新增的方法

2.7.1 default默认方法

接口每新增一个抽象方法,实现类都要重写,不方便升级。

JDK8新增:

  • 允许在接口中定义默认方法,需要使用关键字default修饰
  • 作用:解决接口升级问题
// public default 返回值类型 方法名(参数列表) {}
public default void show() {}
  • 注意事项:默认方法不是抽象方法,所以不强制被重写。但是如果被重写,重写的时候需要去掉default关键字。
  • public可省略,default不可以
  • 如果实现了多个接口,多个接口中存在相同名字(不同参数类型不用重写!不同返回值要重写!)的默认方法,子类就必须对该方法进行重写!!!。(default方法是有方法体的,子类对象不知道调用哪个接口的default方法,所以必须重写)(相同方法名不同参数类型,可以根据被调用的方法进行匹配,而相同方法名同不同返回值则不知道要返回何种类型)

2.7.2 static静态方法

JDK8新增:

  • 允许在接口中定义静态方法,需要用static修饰
// public static 返回值类型 方法名(参数列表) {}
public static void show() {}
  • 注意事项:
  • 静态方法只能通过接口名调用,不能通过实现类名或者对象名调用
  • public可省略,static不可以
  • 静态方法不像defalut方法可以重写,加上@Override会报错。不可以重写(即使是被其他接口继承,在该接口也不能)。(子类把从父类继承下来的虚方法表里面的方法进行覆盖才算重写,而static的方法不属于虚方法表)

2.7.3 private私有方法

接口中存在重复代码,需要抽取出来定义为方法,而该方法不想被外界访问,jdk9后允许定义为私有

JDK9新增的方法:

private

接口中私有方法的定义格式:

  • default类型(!!不用写defalut!!
  • default的私有方法不能为接口的static方法服务。
// private 返回值类型 方法名(参数列表) {}
private void show() {}
  • static类型
  • static的私有方法可以放到接口的default默认方法体中。(不推荐)
// private static 返回值类型 方法名(参数列表) {}
private static void method() {}

2.8 接口的应用

接口版多态,作为方法形参,可以传递实现该接口的类的对象。遵循编译看左边,运行看右边原则。

接口3

  • 接口代表规则,是行为的抽象,想要让哪个类拥有一个行为,就让这个类实现对应的接口就可以了。
  • 当一个方法的参数是接口时,可以传递接口所有实现类的对象,这种方式称之为接口多态。

2.9 适配器设计模式

  • 设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性、程序的重用性。
  • 简单理解:设计模式就是各种套路。
  • 适配器设计模式:解决接口与接口实现类之间的矛盾问题

步骤:

  • 编写中间类XXXAdapter,实现对应的接口
  • 对接口中的抽象方法进行空实现
  • 让真正的实现类继承中间类,并重写需要用的方法
  • 为了避免其他类创建适配器类的对象,中间的适配器类用abstract进行修饰

如上面所说,假如接口中有10个抽象方法,而我只需要其中某一个方法。则可以创建中间类实现接口,空重写,再让那个类去继承这个中间类。

public interface InterAA {
    void method1();
    void method2();
    void method3();
    void method4();
    void method5();
    void method6();
    void method7();
    void method8();
    void method9();
}

// 适配器类,对方法进行空重写  XXXAdapter
public abstract class InterAdapter implements InterAA{

    @Override
    public void method1() {}

    @Override
    public void method2() {}

    // .........

}
// 继承适配器类进行重写
public class InterAAImpl extends InterAdapter{
    @Override
    public void method5() {
        System.out.println("this is method 5");
    }
}

如果最终的实现类还需要继承其他的父类,可以让中间的适配器类去继承该父类。


3.内部类

Java类的五大成员之一:属性,方法,构造方法,代码块,内部类

3.1 概述

3.1.1 什么是内部类

将一个类A定义在另一个类B里面,里面的那个类A就称为内部类,B则称为外部类。可以把内部类理解成寄生,外部类理解成宿主。

3.1.2 什么时候使用内部类

一个事物内部还有一个独立的事物,内部的事物脱离外部的事物无法独立使用

  1. 人里面有一颗心脏。
  2. 汽车内部有一个发动机。
  3. 为了实现更好的封装性。

3.2 内部类的分类

按定义的位置来分

  1. 成员内部类,类定义在了成员位置 (类中方法外称为成员位置,无static修饰的内部类)
  2. 静态内部类,类定义在了成员位置 (类中方法外称为成员位置,有static修饰的内部类)
  3. 局部内部类,类定义在方法内
  4. 匿名内部类,没有名字的内部类,可以在方法中,也可以在类中方法外。

3.3 成员内部类

  • 内部类表示的事务是外部类的一部分
  • 内部类单独出现没有任何意义(ArrayList的内部类迭代器Itr)

内部类的访问特点

  • 内部类可以直接访问外部类的成员,包括私有
  • 外部类要访问内部类的成员,必须创建对象

需求:写一个javabean类描述汽车

属性:汽车的品牌,车龄,颜色,发动机的品牌,使用年限

public class Car {   // 外部类
    String carName; // private
    int carAge;
    String carColor;

    public void show() {
        System.out.println(carName);
        Engine e = new Engine();
        System.out.println(e.engineName);
//        System.out.println(engineAge);
    }

    class Engine {  // 内部类
        String engineName;
        int engineAge;

        public void show() {
            System.out.println(engineAge);
            System.out.println(carName);
        }
    }
}
// ========================================================
public class CarTest {
    public static void main(String[] args) {

        Car car = new Car();
        car.carName = "nihao";
        car.carAge = 10;
        car.carColor = "红";

        car.show();

    }
}

成员内部类特点

  • 无static修饰的内部类,属于外部类对象的。
  • 宿主:外部类对象。

内部类的使用格式

 外部类.内部类。 // 访问内部类的类型都是用 外部类.内部类

获取成员内部类对象的两种方式

方式一:外部直接创建成员内部类的对象(创建外部类对象,让该对象创建内部类对象)

外部类.内部类 变量 = new 外部类().new 内部类();

方式二:在外部类中定义一个方法提供内部类的对象

案例演示

方式一:
public class Test {
    public static void main(String[] args) {
        //  宿主:外部类对象。
       // Outer out = new Outer();
        // 创建内部类对象。
        Outer.Inner oi = new Outer().new Inner();
        oi.method();
    }
}

class Outer {
    // 成员内部类,属于外部类对象的。
    // 拓展:成员内部类不能定义静态成员。
    public class Inner{
        // 这里面的东西与类是完全一样的。
        public void method(){
            System.out.println("内部类中的方法被调用了");
        }
    }
}

方式二:
public class Outer {
    String name;
    private class Inner{
        static int a = 10;
    }
    public Inner getInstance(){
        return new Inner();
    }
}

public class Test {
    public static void main(String[] args) {
        Outer o = new Outer();
        // Outer.Inner inner = o.getInstance();  Test获取不到Inner类,会报错,可以改为Object
        System.out.println(o.getInstance());

    }
}

3.4 成员内部类的细节

编写成员内部类的注意点:

  1. 成员内部类可以被一些修饰符所修饰,比如: private,默认,protected,public,static等
  2. 在成员内部类里面,JDK16之前不能定义静态变量,JDK16开始才可以定义静态变量。
  3. 创建内部类对象时,对象中有一个 隐含的Outer.this记录外部类对象的地址值。(请参见3.6节的内存图)

详解:

  • 内部类被private修饰,外界无法直接获取内部类的对象,只能通过3.3节中的方式二(外部类定义方法getInstance(),方法内返回内部类对象)获取内部类的对象。(相当于类的内部属性只能在类中访问)
  • 被其他权限修饰符修饰的内部类一般用3.3节中的方式一直接获取内部类的对象
  • 内部类被static修饰是成员内部类中的特殊情况,叫做静态内部类下面单独学习。
  • 内部类如果想要访问外部类的成员变量,外部类的变量必须用final修饰,JDK8以前必须手动写final,JDK8之后不需要手动写,JDK默认加上。

https://blog.csdn.net/weixin_43670802/article/details/90381589

  1. 内部类里面使用外部类的局部变量时,其实就是内部类的对象在使用它,内部类对象生命周期中都可能调用它, 而内部类试图访问外部方法中的局部变量时,外部方法的局部变量很可能已经不存在了,那么就得延续其生命, 拷贝到内部类中,而拷贝会带来不一致性,从而需要使用final声明保证一致性。说白了,内部类会自动拷贝外部变量 的引用,为了避免:
  • 外部方法修改引用,而导致内部类得到的引用值不一致
  • 内部类修改引用,而导致外部方法 的参数值在修改前和修改后不一致。于是就用 final 来让该引用不可改变。
  1. 内部类通常都含有回调,引用那个匿名内部类的函数执行完了就没了,所以内部类中引用外面的局部变量需要 是final的,这样在回调的时候才能找到那个变量,而如果是外部类的成员变量就不需要是final的,因为内部类本身 都会含有一个外部类的引用(外部类.this),所以回调的时候一定可以访问到
Outer o = new Outer();
System.out.println(o);

Outer.Inner i = o.new Inner();
System.out.println(i);

// innerclass.Outer@3b07d329
// innerclass.Outer$Inner@404b9385

3.5 成员内部类面试题

请在?地方向上相应代码,以达到输出的内容

注意:内部类访问外部类对象的格式是:外部类名.this

public class Test {
    public static void main(String[] args) {
        Outer.inner oi = new Outer().new inner();
        oi.method();
    }
}

class Outer {   // 外部类
    private int a = 30;

    // 在成员位置定义一个类
    class inner {
        private int a = 20;

        public void method() {
            int a = 10;
            System.out.println(???);    // 10   答案:a
            System.out.println(???);    // 20   答案:this.a
            System.out.println(???);    // 30   答案:Outer.this.a
        }
    }
}

3.5.1 多加几题

需求:在控制台输出”HelloWorld”

interface Inter {
    void show(); 
}
class Outer { 
    //补齐代码 
}
public class OuterDemo {
    public static void main(String[] args) {
        Outer.method().show();
    }
}

Outer.methd().show(),说明Outer.methd()会得到内部类对象,而method()可以直接被Outer类调用,那这个方法应该是static的,**因此内部返回的new Inner()也是静态的,Inner为静态内部类。

public interface Inter {
    void show();
}

public class Outer {

    // private下面会报错
    public static class Inner {
        public void show() {
            System.out.println("HelloWorld");
        }
    }

    public static Inner method() {
        return new Inner();
    }
}

class OuterDemo {
    public static void main(String[] args) {
        // 如果 Inner 是private,则无法创建Inner类对象
        Outer.method().show();

    }
}

需求:在测试类Test中创建A的对象a并调用成员方法methodA(),要求用两种方式实现

public class Test {
    public static void main(String[] args) {    

    }
}

//定义接口
interface InterA {
    void showA();   
}

class A {
    public void methodA(InterA a) {
        a.showA();      
    }   
}

思路:启发点:new 接口。

public class TestA {
    public static void main(String[] args) {
        // 创建出来对象,当做实参
        InterA intera = new InterA() {
            @Override
            public void showA() {
                System.out.println("override AAAA");
            }
        };
        A a = new A();
        a.methodA(intera);

        System.out.println("===================================");
        // 直接在参数里new
        A b = new A();
        b.methodA(new InterA() {
            @Override
            public void showA() {
                System.out.println("override BBBBBBBB");
            }
        });

    }
}

interface InterA {
    void showA();
}

class A {
    public void methodA(InterA a) {
        a.showA();
    }
}

3.6 成员内部类内存图

内部类默认与一个Outer(外部类).this用于记录外部类对象的地址值

内部类内存


3.7 静态内部类

静态内部类特点

  • 静态内部类是一种特殊的成员内部类。
  • 有static修饰,属于外部类本身的。
  • 总结:静态内部类与其他类的用法完全一样。只是访问的时候需要加上外部类.内部类。
  • 拓展1:静态内部类可以直接访问外部类的静态成员。
  • 拓展2:静态内部类不可以直接访问外部类的非静态成员,如果要访问需要创建外部类的对象。
  • 拓展3:静态内部类中没有隐含的Outer.this。

内部类的使用格式

外部类.内部类。

静态内部类对象的创建格式

外部类.内部类  变量 = new  外部类.内部类构造器;
Outer01.Inner01 in  = new Outer01.Inner01("张三");

调用方法的格式:

  • 调用非静态方法的格式:先创建对象,用对象调用
  • 调用静态方法的格式:外部类名.内部类名.方法名();
Outer.Inner.show();

案例演示

// 外部类:Outer01
class Outer01{
    private static  String sc_name = "GDUFE";
    int a = 10;
    // 内部类: Inner01
    public static class Inner01{
        // 这里面的东西与类是完全一样的。
        private String name;
        public Inner01(String name) {
            this.name = name;
        }
        public void showName(){
            System.out.println(this.name);
            // 拓展:静态内部类可以直接访问外部类的静态成员。
            System.out.println(sc_name);
            // 调用非静态的格式:先创建对象,再调用
            System.out.println(new Outer01().a);
        }
    }
}

public class InnerClassDemo01 {
    public static void main(String[] args) {
        // 创建静态内部类对象。
        // 外部类.内部类  变量 = new  外部类.内部类构造器;
        Outer01.Inner01 in  = new Outer01.Inner01("张三");
        in.showName();
    }
}

3.8 局部内部类

定义格式:

class 外部类名 {
    数据类型 变量名;

    修饰符 返回值类型 方法名(参数列表) {
        // …
        class 内部类 {
            // 成员变量
            // 成员方法
        }
    }
}
  • 局部内部类 :定义在方法中的类。类似于方法中的局部变量。
  • 外界是无法直接使用,需要在方法内部创建对象并使用。
  • 该类可以直接访问外部类的成员,也可以访问方法内的局部变量。
public class Outer01 {

    int b = 20;
    public void show() {
        int a = 10;

        class Inner{
            String name;
            int age;

            public void method() {
                System.out.println(a);
                System.out.println(b);
                System.out.println("局部内部类的method方法");
            }
        }

        Inner i = new Inner();
        i.method();

    }
}

3.9 匿名内部类【重点】

3.9.1 概述

匿名内部类 :是内部类的简化写法。他是一个隐含了名字的内部类。开发中,最常用到的内部类就是匿名内部类了。

是隐藏了名字的内部类,可以写在成员位置,也可以写在局部位置。


3.9.2 格式

new 类名或者接口名() {
     重写方法;
};

包含了:

  • 继承或者实现关系
  • 方法重写
  • 创建对象

所以从语法上来讲,这个整体其实是匿名内部类对象(一个类的子类对象或者接口的实现类对象)


3.9.3 什么时候用到匿名内部类

实际上,如果我们希望定义一个只要使用一次的类,就可考虑使用匿名内部类。匿名内部类的本质作用

是为了简化代码

之前我们使用接口时,似乎得做如下几步操作:

  1. 定义子类
  2. 重写接口中的方法
  3. 创建子类对象
  4. 调用重写后的方法
interface Swim {
    public abstract void swimming();
}

// 1. 定义接口的实现类
class Student implements Swim {
    // 2. 重写抽象方法
    @Override
    public void swimming() {
        System.out.println("狗刨式...");
    }
}

public class Test {
    public static void main(String[] args) {
        // 3. 创建实现类对象
        Student s = new Student();
        // 4. 调用方法
        s.swimming();
    }
}

我们的目的,最终只是为了调用方法,那么能不能简化一下,把以上四步合成一步呢?匿名内部类就是做这样的快捷方式。

  1. 把前面的class Student implements Swim去掉,剩余的内容就变成了一个没有名字的类
  2. 这个没有名字的类想要实现Swim接口,把Swim写在大括号的前面,表示这个没有名字的类实现了Swim接口(如果是类,则是自称关系,Swim是后面括号里没有名字的类的父类),所以需要在类中重写接口里面所有的抽象方法。
  3. 如果还想要创建这个没有名字的类的对象,加new()

3.9.4 匿名内部类前提和格式

匿名内部类必须继承一个父类或者实现一个父接口

匿名内部类格式

new 父类名或者接口名(){
    // 方法重写
    @Override 
    public void method() {
        // 执行语句
    }
};

3.9.5 使用方式

以接口为例,匿名内部类的使用,代码如下:

interface Swim {
    public abstract void swimming();
}

public class Demo07 {
    public static void main(String[] args) {
        // 使用匿名内部类,new Swim(){} 相当于接口实现类对象
        new Swim() {
            @Override
            public void swimming() {
                System.out.println("自由泳...");
            }
        }.swimming();

        // 接口 变量 = new 实现类(); // 多态,走子类的重写方法
        Swim s2 = new Swim() {
            @Override
            public void swimming() {
                System.out.println("蛙泳...");
            }
        };
        // 上面可以lambda简化:
        // Swim s2 = () -> System.out.println("override swimming");

        s2.swimming();
        s2.swimming();
    }
}

3.9.6 匿名内部类的特点

  1. 定义一个没有名字的内部类
  2. 这个类实现了父类,或者父类接口
  3. 匿名内部类会创建这个没有名字的类的对象

3.9.7 匿名内部类的使用场景

通常在方法的形式参数是接口或者抽象类时,也可以将匿名内部类作为参数传递。代码如下:

interface Swim {
    public abstract void swimming();
}

public class Demo07 {
    public static void main(String[] args) {
        // 普通方式传入对象
        // 创建实现类对象
        Student s = new Student();

        goSwimming(s);
        // 匿名内部类使用场景:作为方法参数传递
        Swim s3 = new Swim() {
            @Override
            public void swimming() {
                System.out.println("蝶泳...");
            }
        };
        // 传入匿名内部类
        goSwimming(s3);

        // 完美方案: 一步到位
        goSwimming(new Swim() {
            public void swimming() {
                System.out.println("大学生, 蛙泳...");
            }
        });

        goSwimming(new Swim() {
            public void swimming() {
                System.out.println("小学生, 自由泳...");
            }
        });
    }

    // 定义一个方法,模拟请一些人去游泳
    public static void goSwimming(Swim s) {
        s.swimming();
    }
}

匿名内部类括号里的相当于Swim的实现类,因此可以传入goSwimming方法中,类似于Swim s = 子类对象 多态。


3.9.8 匿名内部类用this

chatGPT answers

在Java匿名内部类中,关键字this通常用于引用外部类的实例或方法。由于匿名内部类没有名称,因此它无法直接使用外部类的实例或方法。在这种情况下,可以使用外部类.this来引用外部类的实例,或者使用外部类.this.method()来调用外部类的方法。

以下是一些示例代码:

public class OuterClass {
    private int value = 10;

    public void doSomething() {
        ActionListener listener = new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                // 在这里,需要使用外部类实例的值
                int newValue = OuterClass.this.value + 1;
                System.out.println("New value is: " + newValue);
            }
        };

        // 把 listener 添加到按钮中
        JButton button = new JButton("Click me!");
        button.addActionListener(listener);
    }
}

在这个例子中,我们在doSomething()方法中创建了一个ActionListener匿名内部类。在actionPerformed()方法中,我们需要访问外部类的value变量,并给它加上1。由于匿名内部类中没有this关键字来引用外部类的实例,所以我们使用了OuterClass.this.value来访问外部类的value变量。

另一个示例,如果我们需要在匿名内部类中调用外部类的方法,可以像这样:

public class OuterClass {
    public void doSomething() {
        ActionListener listener = new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                // 在这里,需要调用外部类的方法
                OuterClass.this.myMethod();
            }
        };

        // 把 listener 添加到按钮中
        JButton button = new JButton("Click me!");
        button.addActionListener(listener);
    }

    public void myMethod() {
        System.out.println("Hello, world!");
    }
}

在这个例子中,我们在actionPerformed()方法中调用了OuterClass.this.myMethod()来调用外部类的myMethod()方法。