Java编程基础知识之接口篇

18 篇文章 2 订阅
11 篇文章 0 订阅

前言

        本文简单介绍接口、lambda表达式、内部类等java编程基础知识,本文例子提供的比较多,篇幅比较大,设计接口定义、实现comparable接口与Comparator<T>接口以及二者区别,拷贝的系列问题和注意事项;lambda表达式的各种情况及其升级版:方法引用;内部类介绍了普通内部类、静态内部类、匿名内部类、局部内部类,并且对内部类进行了反编译处理来查看底层原来,简单解释内部类带来的内存泄漏问题

目录

前言

一、接口

1.接口概念

2.定义接口

3. 实现多个接口

4.回调函数

(1)同步回调

(2)异步回调

5.类实现Comparable接口

6.实现comparator接口

7.Comparable接口和Comparator接口之间的差异

8.接口间的继承

9.Cloneable接口和深拷贝 

1)标志接口 

2)实现Cloneable接口

3)Object类的clone()

4)浅拷贝VS深拷贝

10.抽象类与接口的区别

11.接口使用注意事项:

1)解决默认方法冲突

二、lambda表达式

1.使用lamdba表达式好处

2.lambda表达式语法

1)语法:

2)实例:

3.函数式接口

4.变量作用域(lambda使用注意事项)

5.方法引用

1)说明

2)使用

3)构造器引用

三、内部类

1.普通内部类

2.静态内部类

3.匿名内部类

4.局部内部类

5.深入理解内部类

6.内存泄漏


一、接口

1.接口概念

        官方解释:Java接口是一系列方法的声明,是一些方法特征的集合。

        我的看法: 接口是抽象类的更进一步,1000%纯度的抽象,是一组需求。正常情况下,Java无法实现一个类继承多个类,所以诞生一个接口来处理这个问题,一个类可以继承多个接口。

2.定义接口

       接口中包含的方法都是抽象方法, 字段只能包含静态常量。一个类实现接口,就要把它内部的抽象方法给重写。

public class InterfaceDemo {

    public static void main(String[] args) {
        //可以使用接口类引用子类
        Ieating cat = new Animal("fish" , "cat");
        cat.eat();
    }
}
//接口
interface Ieating{
    public final static int age = 10;
    void eat();
}
//实现接口
class Animal implements Ieating{
    String food;
    String name;
    public Animal(String food,String name){
        this.food = food;
        this.name = name;
    }
    @Override
    public void eat(){
        System.out.println(this.name + "爱吃" + this.food);
    }
}
  •  使用interface定义一个接口
  • 接口中的方法一定是抽象方法,因此可以省略abstract
  • 接口中的方法一定是public,因此可以省略public
  • Animal使用Implement继承接口,此时表达含义不再是“扩展”,而是实现接口
  • 在调用的时候同样可以创建一个接口的引用,对应到一个子类的实例
  • 接口不能被单独实例化

        接口中只能包含抽象方法,对于字段来说,接口中只能包含静态常量(final static)

interface Ieating{
    public final static int age = 10;
    void eat();
}

        其中的 public, static, fifinal 的关键字都可以省略. 省略后的 仍然表示 public 的静态常量

提示 :
1. 我们创建接口的时候 , 接口的命名一般以大写字母 I 开头 .
2. 接口的命名一般使用 " 形容词 " 词性的单词 .
3. 阿里编码规范中约定 , 接口中的方法和属性不要加任何修饰符号 , 保持代码的简洁性

         一个错误代码:

//接口
interface Ieating{
    int age = 10;
    void eat();
}
//实现接口
class Animal implements Ieating{
   
    @Override
    //由于默认为default权限,比接口内定义的eat()public 权限低,所以无法覆盖,出现错误
    void eat(){    
        System.out.println(this.name + "爱吃" + this.food);
    }
}
完整格式简化
interface Ieating{
    public final static int age = 10;
    public abstract void eat();
}
interface Ieating{
    int age = 10;
    void eat();
}

        除此之外,接口内也可以有默认方法:

interface Ieating{
    int age = 10;
    void eat();
    
    //默认实现
    default int aa(){
        .....
    }
}

        默认方法可以解决,接口增加非默认方法带来的“源代码兼容问题”和未重新编译直接调用的问题 

3. 实现多个接口

        有的时候我们需要让一个类同时继承自多个父类. 这件事情在有些编程语言通过 多继承 的方式来实现的 . 然而 Java 中只支持单继承 , 一个类只能 extends 一个父类 . 但是可以同时实现多个接口 , 也能达到多继承类似的效果 . 现在我们通过类来表示一组动物

 动物类:

class Animal { 
    protected String name; 
 
    public Animal(String name) { 
        this.name = name; 
    } 
}

 提供一组接口, 分别表示 "会飞的", "会跑的", "会游泳的”

interface IFlying {
    void fly();
}
interface IRunning {
    void run();
}
interface ISwimming {
    void swim();
}

创建几个具体的动物  

猫会跑

    class Cat extends Animal implements IRunning {
        public Cat(String name) {
            super(name);
        }
        @Override
        public void run() {
            System.out.println(this.name + "正在用四条腿跑");
        }
    }

 鱼会游

    class Fish extends Animal implements ISwimming {
        public Fish(String name) {
            super(name);
        }
        @Override
        public void swim() {
            System.out.println(this.name + "正在用尾巴游泳");
        }
    }

青蛙, 既能跑, 又能游(两栖动物)

    class Frog extends Animal implements IRunning, ISwimming {
        public Frog(String name) {
            super(name);
        }
        @Override
        public void run() {
            System.out.println(this.name + "正在往前跳");
        }
        @Override
        public void swim() {
            System.out.println(this.name + "正在蹬腿游泳");
        }
    }

 还有一种神奇的动物, 水陆空三栖, 叫做 "鸭子"

    class Duck extends Animal implements IRunning, ISwimming, IFlying {
        public Duck(String name) {
            super(name);
        }
        @Override
        public void fly() {
            System.out.println(this.name + "正在用翅膀飞");
        }
        @Override
        public void run() {
            System.out.println(this.name + "正在用两条腿跑");
        }
        @Override
        public void swim() {
            System.out.println(this.name + "正在漂在水上");
        }
    }
上面的代码展示了 Java 面向对象编程中最常见的用法 : 一个类继承一个父类 , 同时实现多种接口 .
继承表达的含义是 is - a 语义, 而接口表达的含义是 具有 xxx 特性 .
        这样设计有什么好处呢? 时刻牢记多态的好处 , 让程序猿 忘记类型 . 有了接口之后 , 类的使用者就不必关注具体类型 , 而只关注某个类是否具备某种能力.

4.回调函数

        回调的本质:A类a方法调用B类的b方法,b方法在调用A类的c方法

(1)同步回调

举个例子:班长问我“1+1”等于多少,我处理这个问题,然后给他提供反馈

public class CallbackDemo {
    public static void main(String[] args) {
        Monitor m = new Monitor();
        m.weekJob();
    }
}
//A类
class Monitor implements ICallBack{
    @Override
    //c方法
    public void setResult(String result){
        System.out.println(result);
    }
    //此处调用B类b方法
    public void weekJob(){
        System.out.println("班长发布了任务");
        Follower f = new Follower();
        //将A类引用传递给B类的d方法
        f.doingSomething(this);
    }
}
//B类
class Follower{
    //d方法,调用A类c方法
    public void doingSomething(ICallBack call){
        call.setResult("做完啦~~");
    }
}
interface ICallBack{
    void setResult(String result);
}

//结果
班长发布了任务
做完啦~~

        这里还涉及一个向上转型的问题,this传递给ICallBack,间接完成了下面这个代码:

ICallBack call = new Monitor();

(2)异步回调

        同步回调是当下完成并上交反馈,而异步回调是忙完我手头上的东西后,来调用你A的c方法来给你提供回馈,并且在我忙的时候,班长也不应该傻傻等我忙完后在给他答案,而是应该去做他自己的事情,这个时候我们就需要使用线程来帮忙了

public class CallbackDemo2 {
    public static void main(String[] args) {
        Monitor2 m = new Monitor2();
        m.weekJob();
        System.out.println("班长卷去了");
    }
}
//A类
class Monitor2 implements ICallBack2{
    @Override
    //c方法
    public void setResult(String result){
        System.out.println(result);
    }
    //此处调用B类b方法
    public void weekJob(){
        System.out.println("班长发布了任务");
        Follower2 f = new Follower2();
        //将A类引用传递给B类的d方法
        f.doingSomething(this);
    }
}
//B类
class Follower2{
    //d方法,调用A类c方法
    public void doingSomething(ICallBack2 call){
        //此处创建一个线程
        new Thread(()->{
            //这个就是B要干的事情,忙完在执行班长的任务
            for(int i  = 0 ; i < 10 ; i++){
                
            }
            call.setResult("做完啦~~");
        }).start();

    }
}
interface ICallBack2{
    void setResult(String result);
}


//结果
班长发布了任务
班长卷去了
做完啦~~

        如果此处没有创建多一个线程,那么语句是一条一条执行的,为同步回调 

5.类实现Comparable接口

        类实现Comparable接口,并实现其中compareTo方法,有利于使用排序函数

 例子:

给定一个学生类

class Student {
    private String name;
    private int score;
    public Student(String name, int score) {
        this.name = name;
        this.score = score;
    }

    @Override
    public String toString() {
        return "[" + this.name + ":" + this.score + "]";
    }
}

        再给定一个学生对象数组, 对这个对象数组中的元素进行排序(按分数降序)

Student[] students = new Student[] {
    new Student("张三", 95),
    new Student("李四", 96),
    new Student("王五", 97),
    new Student("赵六", 92),
};

         数组有一个现成的sort方法,但是直接使用会报异常

    Arrays.sort(students);
    System.out.println(Arrays.toString(students));
    // 运行出错, 抛出异常. 
    Exception in thread "main" java.lang.ClassCastException: Student cannot be cast to
    java.lang.Comparable

        其实不难想到,Arrays.sort()方法就是给数值服务的,它可以直接判断大小,但是类它没有方式去判断大小,所以需要我们额外指定,指定的方式就是类实现Comparable接口,并实现其中的compareTo方法

//实现接口
class Student implements Comparable {
    private String name;
    private int score;
    public Student(String name, int score) {
        this.name = name;
        this.score = score;
    }
    //重写toString()
    @Override
    public String toString() {
        return "[" + this.name + ":" + this.score + "]";
    }
    //重写compareTo()方法
    @Override
    public int compareTo(Object o) {
        Student s = (Student)o;
        if (this.score > s.score) {
            return -1;
        } else if (this.score < s.score) {
            return 1;
        } else {
            return 0;
        }
    }
}

         在 sort 方法中会自动调用 compareTo 方法. compareTo 的参数是 Object , 其实传入的就是 Student 类型的对象.

然后比较当前对象和参数对象的大小关系(按分数来算).

  • 如果当前对象应排在参数对象之前, 返回小于 0 的数字;
  • 如果当前对象应排在参数对象之后, 返回大于 0 的数字;
  • 如果当前对象和参数对象不分先后, 返回 0;
再次执行程序 , 结果就符合预期了
// 执行结果
[[王五:97], [李四:96], [张三:95], [赵六:92]]
注意事项 : 对于 sort 方法来说 , 需要传入的数组的每个对象都是 " 可比较 " , 需要具备 compareTo 这样的能力 . 通过重写 compareTo 方法的方式 , 就可以定义比较规则 .

        为了进一步加深对接口的理解, 我们可以尝试自己实现一个 sort 方法来完成刚才的排序过程(使用冒泡排序)

    public static void sort(Comparable[] array) {
        for (int bound = 0; bound < array.length; bound++) {
            for (int cur = array.length - 1; cur > bound; cur--) {
                if (array[cur - 1].compareTo(array[cur]) > 0) {
                    // 说明顺序不符合要求, 交换两个变量的位置
                    Comparable tmp = array[cur - 1];
                    array[cur - 1] = array[cur];
                    array[cur] = tmp;
                }
            }
        }
    }

        再次执行代码  

sort(students); 
System.out.println(Arrays.toString(students)); 

// 执行结果
[[王五:97], [李四:96], [张三:95], [赵六:92]]

6.实现comparator接口

        上一节解释了如何对一个对象数组进行排序。现在我们来学习排序的第二个版本利用比较器Comparator<T>接口

        查看Java API文档

        这个sort方法要求我们提供一个类型的数组,然后提供一个比较器,然后这个方法以这个比较器为工具来进行排序,T就是指这个要排序的类型

        但是我们知道,一个接口是不能被直接创建实例的,所以我们需要创建一个匿名接口的对象,然后通过接口类型变量来进行保存

    Comparator<Integer> c = new Comparator<Integer>() {
        @Override
        public int compare(Integer o1, Integer o2) {
            return o1 - o2;
        }
    };

        下面是实操例子:

    public static void main(String[] args) {
        //Integer包装器类数组
        Integer[] arr = {1,2,3,4,8,7,9,2,2,4,2};
        //比较器引用和重写
        Comparator<Integer> c = new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o1 - o2;
            }
        };
        //传递比较数组和重写比较器
        Arrays.sort(arr , c);
        for(Integer i : arr ){
            System.out.println(i);
        }
    }

7.Comparable接口和Comparator接口之间的差异

         二者都是完成一个辅助比较的任务,前者是一个类要去继承接口,来保证这个类能被sort()之间进行比较然后排序,后者则是在类没有实现Comparable接口的时候,自定义一个比较器来进行传参比较,前者多用于继承完成辅助任务,后者多用于自身创建匿名对象来完成任务

8.接口间的继承

        接口可以继承一个接口, 达到复用的效果 . 使用 extends 关键字 .
interface IRunning { 
    void run(); 
} 
interface ISwimming { 
    void swim(); 
} 
// 两栖的动物, 既能跑, 也能游
interface IAmphibious extends IRunning, ISwimming { 

} 
class Frog implements IAmphibious { 
 ... 
}

9.Cloneable接口和深拷贝 

1)标志接口 

        Cloneable接口是Java提供的少数标记接口之一。Comparable等接口的通常用途是确保一个类实现一个或一个组特点的方法。而与这些接口不一样的是,标记接口不包含任何方法,它的唯一作用就是允许在类型查询中使用instanceof。

        所以由此可见,我们克隆使用的是从超类Object继承来的。如果一个对象请求克隆,但是没有实现这个接口,就会生成一个检查类型异常

2)实现Cloneable接口

        Java 中内置了一些很有用的接口 , Clonable 就是其中之一 . Object 类中存在一个 clone 方法 , 调用这个方法可以创建一个对象的 " 拷贝 ". 但是要想合法调用 clone 方法, 必须要先 实现 Clonable 接口, 否则就会抛出 CloneNotSupportedException 异常.
public class CloneableDemo {
    public static void main(String[] args) {
        Animal animal = new Animal();
        //调用克隆方法
        Animal animal2 = animal.clone();
        System.out.println(animal == animal2);
    }
}
//实现接口
class Animal implements Cloneable {
    private String name;
    @Override
    public Animal clone() {
        //创建一个Animal的空对象
        Animal o = null;
        try {
            //这里调用Object的克隆方法克隆Object对象,然后强制类型转化
            o = (Animal)super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return o;
    }
}

// 输出结果
// false

        在这里有一点值得注意, Object类是对Animal一无所知,所以只能通过逐个字段进行拷贝

3)Object类的clone()

        先进行内容回顾:

        protected修饰的方法,在不同包时候,子类可以访问该方法,但是不能在包外创建新的对象调用方法。

        即protected权限成员可以被非同包的子类访问,指子类内部可以直接使用父类protected成员;而不是在外部创建子类对象,通过子类对象访问父类protected成员。可以到此网页【菜鸟教程】对protected进行更好的了解

        Object类是在lang包下的,而它的clone()是一个protected方法,当在其他包下的时候,不能通过子类的创建来调用clone() ,但是可以被子类自身直接调用,例子如下:

    public class ClonableDemo {
        public static void main(String[] args) throws CloneNotSupportedException {
            IAnimal animal = new IAnimal();
            //报出异常
            //'clone()' 在 'java.lang.Object' 中具有 protected 访问权限
            IAnimal animal2 = animal.clone();
            System.out.println(animal == animal2);
        }
    }

    //IAnimal内部可以调用lang包下的Object的clone()
    class IAnimal implements Cloneable {
        private String name;
        public IAnimal() throws CloneNotSupportedException{
            IAnimal a = (IAnimal) super.clone();
        }
   }

        所以我们必须立刻马上紧急重写clone() ,并将其的权限修改为public ,这样所以方法就都能对类的clone()进行调用

class IAnimal implements Cloneable {
    private String name;
    @Override
    //修改权限为public
    public IAnimal clone() throws CloneNotSupportedException {
         ......
    }
}

4)浅拷贝VS深拷贝

        Cloneable拷贝出的对象是一份“浅拷贝”

public class Test {
    static class A implements Cloneable {
        public int num = 0;
        @Override
        public A clone() throws CloneNotSupportedException {
            return (A)super.clone();
        }
    }
    static class B implements Cloneable {
        public A a = new A();
        @Override
        public B clone() throws CloneNotSupportedException {
            return (B)super.clone();
        }
    }
    public static void main(String[] args) throws CloneNotSupportedException {
        B b = new B();
        B b2 = b.clone();
        b.a.num = 10;
        System.out.println(b2.a.num);
    }
}
// 执行结果
10
        通过 clone 拷贝出的 b 对象只是拷贝了 b 自身, 而没有拷贝内部包含的 a 对象 . 此时 b b2 中包含的 a 引用仍然是指向同一个对象. 此时修改一边 , 另一边也会发生改变 .
        

        如果想要实现深拷贝,这里就需要也对b内的a进行拷贝(当类多的时候,就需要进行套娃了)

    static class A implements Cloneable {
        public int num = 0;
        @Override
        public A clone() throws CloneNotSupportedException {
            return (A)super.clone();
        }
    }
    static class B implements Cloneable {
        public A a = new A();
        @Override
        public B clone() throws CloneNotSupportedException {
            //将a进行拷贝并赋值
            this.a = a.clone();
            return (B)super.clone();
        }
    }

10.抽象类与接口的区别

        核心区别: 抽象类中可以包含普通方法和普通字段 , 这样的普通方法和字段可以被子类直接使用 ( 不必重写 ), 而接口中不能包含普通方法, 子类必须重写所有的抽象方法 .

11.接口使用注意事项:

1)解决默认方法冲突

        如果先在一个接口中将一个方法定义为默认方法,然后又在超类或者另一个接口中定义同样 的方法。会出现二义性冲突

        Java给出的规则是:

  • 超类优先:如果一个类提供了一个具体方法,同名而且相同的参数类型的默认方法会被忽略
  • 接口冲突:如果一个接口提供了一个默认方法,另一个接口提供了一个同名而且参数类型(不论是否是默认方法)相同的参数方法,必须覆盖这个方法来解决冲突

二、lambda表达式

        lambda表达式采用一种简洁的语法定义代码块,它是一个可以传递的代码块,可以在之后被执行一次或者多次

1.使用lamdba表达式好处

        在上面接口内容时,有提到Comparator<T>接口,我们在使用它的时候,需要给它创建匿名对象,并且进行引用,事实上,真正起到作用的被重写的compare()方法,它才是被sort()方法一直重复调用的。由于代码段在Java中无法进行直接传递,所以诞生出来lambda表达式来解决这一问题。

2.lambda表达式语法

1)语法:

        格式:() -> {}

  • 左侧:指定了lambda表达式需要所有的参数
  • 右侧:制定了lambda体,即lambda表达式要执行的功能
(parameters) -> expression
或
(parameters) ->{ statements; }

 以下是lambda表达式的重要特征:

  • 可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
  • 可选的参数圆括号:一个参数无需定义圆括号,但无参数或多个参数需要定义圆括号。
  • 可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
  • 可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定明表达式返回了一个数值。

 以下是lambda表达是各种情况操作代码:

        无参,无返回值,只有一条执行语句

实现Runnable接口
Thread a = new Thread(()-> System.out.println());

         一个参数,无返回值

Consumer<String> consumer = (x)-> System.out.println(x);

         多个参数,多条语句

Comparator<String> c = (o1, o2) -> {
    System.out.println();
    return o1.length() - o2.length();
};

        多个参数,一条语句,有返回值,省略return 

Comparator<String> c = (o1, o2) -> o1.length() - o2.length();

2)实例:

        回到Comparetor<T>接口的例子:

    public static void main(String[] args) {
        Integer[] arr = {1,2,3,4,8,7,9,2,2,4,2};
        Comparator<Integer> c = new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o1 - o2;
            }
        };
        Arrays.sort(arr , c);
        for(Integer i : arr ){
            System.out.println(i);
        }
    }

        下面利用lambda表达式实现上面Comparator<T> 接口例子

    public static void main(String[] args) {
        Integer[] arr = {1,2,3,4,8,7,9,2,2,4,2};
        Arrays.sort(arr , (o1 ,  o2)-> o1 - o2);
        for(Integer i : arr ){
            System.out.println(i);
        }
    }

        类型参数可以省略,JVM可以通过上下文来进行推断,在执行javac编译程序时,JVM根据程序上下文推断了参数的类型。lambda表达式依赖上下文环境

3.函数式接口

        对于只有一个抽象方法的接口,需要这种接口的对象时,就也可以使用lambda表达式,,这种接口也被称为函数式接口

        我们自定义函数式接口时,要使@FunctionalInterface注解,这样可以用于检测它是否是一个函数式接口,同时javadoc也会包含一条声明,说明这个接口是函数式接口

4.变量作用域(lambda使用注意事项)

        lambda表达式只能引用标记了的final外层局部变量,意识就是说,lambda不能在内部修改这个变量的值,否则会出现编译错误

    final static int num = 10;
    public static void main(String[] args) {

        IA a = ()-> System.out.println(num);
    }
    interface IA{
        void print();
    }

        可以使用lambda表达式访问外层的局部变量

    public static void main(String[] args) {
        final int num = 10;
        IA a = ()-> System.out.println(num);
    }
    interface IA{
        void print();
    }

        lambda表达式可以引用没有被final修饰的变量,但是千万不能在内部进行修改(相当于心中无修改,调用自然神)

    public static void main(String[] args) {
        int num = 10;
        IA a = ()-> System.out.println(num);
    }
    interface IA{
        void print();
    }

        lambda表达式不允许声明一个局部变量同名参数或者局部变量

    //错误示例
    public static void main(String[] args) {
        final int num = 10;
        //报错,出现重名num
        IA a = (num ,num1)-> System.out.println();
    }
    interface IA{
        void print(int num1 , int num2);
    }

    
    //正确示例
    public static void main(String[] args) {
        final int num = 10;
        IA a = (num1 ,num2)-> System.out.println();
    }
    interface IA{
        void print(int num1 , int num2);
    }

         在lambda表达式中使用this关键字时,是指创建这个lambda表达式的方式的this参数。

class Application{
    public void init(){
        ActionListener lister = (event)->{
            System.out.println(this.toString());
            ........
        }
    }
}

         表达式this.toString() 会调用Application 对象的toString() 方法,而不是ActionListener实例的方法,毕竟接口不属于类,也继承不了Object内部定义的toString()

5.方法引用

1)说明

        有时,lambda表达式涉及一个方法。例如,假设你希望只要出现一个定时器事件就打印这个事件对象。当然,为此也可以调用:

var timer =  new Timer(1000 , (event) -> System.out.println(event));

        但是,可以直接把println方法直接传递给Timer构造器

var timer = new Timer(1000 , System.out::println);

         表达式System.out::println是一个方法引用,它指示编译器生成一个函数式的接口的实例,覆盖这个接口的抽象方法来调用给定的方法。在这个例子中,会生成一个ActionListener,它的actionPerformaed(ActionEvent e)方法的调用System.out.println(e).

2)使用

        假设要对字符串进行排序,而不是考虑字母的大小。可以传递以下方式表达式:

Arrays.sort(Strings,String::comparaToIngnoreCase);

        从这些例子可以得知,要运用 ::  运算符分隔方法名与对象或类名。主要是3种情况:

  • object::instanceMethod
  • Class::instanceMethod
  • Class::staticMethod

         第一种情况,方法引用等价于向方法传递参数lambda表达式。对于System.out::println,对象是System.out,所以方法表达式等价于:

(x)->System.out.println(x);

        对于第二种情况,第1个参数会成为方法的隐式参数。例如,String::compareToIngoreCase等同于:

(x,y)->x.comparaToIngnoreCase(y)

        对于第三种情况,所以参数都传递到静态方法:Math::pow等价于:

(x,y)->Math.pow(x , y)

         注意,只有当lambda表达式的体只调用一个方法而不做其他操作时,才能把lambda表达式重写为方法引用。考虑一下lambda表达式:

s->s.length() == 0

        这里有一个方法调用。但是还有一个比较,所以这里不能使用方法引用 

3)构造器引用

        与方法引用一样,但是方法名为new

Class::new 

        对于存在多个构造器,编译器会自己根据上下文进行推倒,例如:int[]::new是一个构造器的引用,它有一个参数即数组长度。等价于:

(x)-> new int[x]

三、内部类

        此节特别声明,以下内将内部类外面的一层统称为外部类

1.普通内部类

public class InnerClassDemo {
    class test1{
        
    }
}

        普通的内部类依赖于外部类的存在而存在,也就是说,要想创建test1类就需要将InnerClassDemo进行创建

public class InnerClassDemo {
    final int oField1= 4;
    private int oField2 = 5;
    public int oField3 = 6;
    public InnerClassDemo(){
        Test1 inner = new Test1();
        System.out.println("其内部类field1字段值为:"+ inner.field1);
        System.out.println("其内部类field2字段值为:"+ inner.field2);
        System.out.println("其内部类field3字段值为:"+ inner.field3);
    }
    class Test1{
        final int field1= 1;
        private int field2 = 2;
        public int field3 = 3;
        public Test1(){
            System.out.println("其外部类field1字段值为:" + oField1);
            System.out.println("其外部类field2字段值为:" + oField2);
            System.out.println("其外部类field3字段值为:" + oField3);
            System.out.println("==============================");
        }
    }
    public static void main(String[] args) {
        new InnerClassDemo();
    }
}


//结果
其外部类field1字段值为:4
其外部类field2字段值为:5
其外部类field3字段值为:6
==============================
其内部类field1字段值为:1
其内部类field2字段值为:2
其内部类field3字段值为:3

        这里可以看出内部类可以访问外部类的字段,外部类也可以访问内部类的字段,后面我们会从底层研究这个问题    

2.静态内部类

        一个类的静态成员,不属于类的任何一个对象,只属于类本身,只要在能访问到这个类的地方,就能通过类名.静态成员进行访问,静态内部类也不例外,它独属于这个类本身,不依靠类的对象创建而存在

public class InnerClassDemo {

    final int oField1= 4;
    private int oField2 = 5;
    public int oField3 = 6;

    public InnerClassDemo(){
        Test2 inner = new Test2();
        System.out.println("其内部类field1字段值为:"+ inner.field1);
        System.out.println("其内部类field2字段值为:"+ inner.field2);
        System.out.println("其内部类field3字段值为:"+ inner.field3);
    }

    static class Test2{
        final int field1= 1;
        private int field2 = 2;
        public int field3 = 3;

        public Test2(){
            // 编译错误,原因很简单,static存在比外部类早,故无法访问到字段
            // System.out.println("其外部类field1字段值为:" + oField1);
            System.out.println("我是" + Test2.class.getName() + "对象");
        }
    }

    public static void main(String[] args) {
        //创建静态内部类,不依赖于外部类而存在
        // new InnerClassDemo.Test2();
        new InnerClassDemo(1);
    }
}


//结果
我是learner.InnerClassDemo$Test2对象
其内部类field1字段值为:1
其内部类field2字段值为:2
其内部类field3字段值为:3

        还是一样外部类能访问到内部类的私有成员 

3.匿名内部类

        匿名类,我们在前面的接口和lambda表达式中有使用过,其手法就是直接new一个接口/类对象,对于接口,我们还需要对接口方法进行重写;对于类,我们直接new一个       

public class InnerClassDemo {
    final int oField1= 4;
    private int oField2 = 5;
    public int oField3 = 6;
    //内部接口
    interface Itest3{
        void test();
    }
    class Test4{
        public Test4(){
            System.out.println("其外部类field1字段值为:" + oField1);
            System.out.println("其外部类field2字段值为:" + oField2);
            System.out.println("其外部类field3字段值为:" + oField3);
            System.out.println("==============================");
        }
    }
    public InnerClassDemo(){
        //这个就是匿名内部类
        new Itest3(){
            @Override
            public void test() {

            }
        };
        //匿名内部类
        new Test4();
        //因为是匿名对象,外部类没办法获取到名字,所以无法访问到内部类的字段
    }
    public static void main(String[] args) {
        //这个创建方式,InnerClassDemo对象也是匿名类
        new InnerClassDemo();
    }
}


//结果
其外部类field1字段值为:4
其外部类field2字段值为:5
其外部类field3字段值为:6

4.局部内部类

         声明局部内部类不能有访问说明符(public 或者 private)。局部内部类的作业域被限定在声明这个局部类的块中

public class InnerClassDemo {
    final int oField1= 4;
    private int oField2 = 5;
    public int oField3 = 6;
    public InnerClassDemo(){
        //局部内部类A
        class A {
            // static int field = 1; // 编译错误!局部内部类中不能定义 static 字段
            public A() {
                System.out.println("创建 " + A.class.getSimpleName() + " 对象");
                System.out.println("其外部类的 field1 字段的值为: " + oField1);
                System.out.println("其外部类的 field2 字段的值为: " + oField2);
                System.out.println("其外部类的 field3 字段的值为: " + oField3);
            }
        }
        A a = new A();
        
        if (true) {
            // 局部内部类 B,只能在当前代码块中使用
            class B {
                public B() {
                    System.out.println("创建 " + B.class.getSimpleName() + " 对象");
                    System.out.println("其外部类的 field1 字段的值为: " + oField1);
                    System.out.println("其外部类的 field2 字段的值为: " + oField2);
                    System.out.println("其外部类的 field3 字段的值为: " + oField3);

                }
            }
            B b = new B();
        }
    }

    public static void main(String[] args) {
        new InnerClassDemo();
    }
}

//结果
创建 A 对象
其外部类的 field1 字段的值为: 4
其外部类的 field2 字段的值为: 5
其外部类的 field3 字段的值为: 6
创建 B 对象
其外部类的 field1 字段的值为: 4
其外部类的 field2 字段的值为: 5
其外部类的 field3 字段的值为: 6

        局部内部类也可以访问外部类的字段,但是外部类无法访问局部内部类的字段,原因在于,局部内部类只在局部有效,出了这个代码块就会失效,所以外部类根本拿不到字段 

5.深入理解内部类

        对于外部类的private字段和内部类的private,理应都只能被其本身调用到,而无法被其他类调用到,但是实事相反,二者都互相偷看到了对方的private字段。  

        《Java编程思想》给出来的说法是:内部类可以访问外部类任何成员,外部类在创建内部类的对象后,也可以访问内部类任何成员

        《Java核心卷I》给的说法是:内部类保存了外部类的引用,而这个引用是无法被查看的      

        这里我们用编译的手段去研究这个问题,先创建一个简单的内外部类先:

public class InnerTest {
    int field1 = 1;
    private int field2 = 10;
    
    public InnerTest(){
        TestA a = new TestA();
        int num = a.x2;
    }
    public class TestA{
        int x1 = field1;
        private int x2 = field2;
        
    }
}

 这里我们要对这个类进行编译与反编译操作:(基于java 16.0.1,有的jdk版本反编译不太一样)

编译:javac InnerTest.java

此时生成了两个.class文件,

反编译:javap -c innerTest.class  和javap -c innerTest$TestA.class

生成两个字节码文件

        对于外部类,它在这个jdk版本是通过getfield方法来获取字段的,这个就有点像是反射而得到的内部类字段。

        对于内部类,以这种特殊方式查看到了它对外部类的引用,并且使用这个引用查看并捕获到了field1和field2(getfield 与putfield)

         特别说明,如果你是jdk 8 版本,它使用的并不是getField方法,而是一个叫做access的静态方法来获取各自的private字段,感兴趣可以去查一查试一试

6.内存泄漏

         到这来相信你已经对内部类有一定的理解。基于此,可以简单说一下内部类引发的内存泄漏,内存泄漏其实就是内存被开辟了,但是没有回收掉,就像C语言中的malloc函数,分配内存,但是没有feel掉,就导致某个程序一直占有这块内存地址。虽然说Java比较幸运,有自己的自动回收垃圾内存机制,但是我们刚刚也学习过内部类,像非静态内部类都是依靠外部类而存在的内部类,当这个外部类的工作结束了,它的内部类还在继续,这样外部类所占用的那一块内存就没办法被回收,造成内存泄漏,相比之下,静态内部类是不依赖外部类存在的,所以外部类结束工作,内存回收对静态内部类基本没影响

感谢观看

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

摸鱼儿hzj

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值