面试阿里,字节跳动美团90%会被问到的面试题【内部类】,你还没掌握吗?
1. 内部类的含义
知道内部类这个概念,除了在用链表时定义节点类时,其余情况具体怎么使用感觉很生疏。再次回顾到这个知识点了,做一个系统的总结
内部类,从字面意思上理解为 “定义在类内部的类”。可以把它理解为汽车的发动机,只能在汽车的内部使用,给火车它就用不了了;人体的心脏,在人体里面维持着血液循环,拿出来人就凉了。这些 “内部的部件”,是只能够依赖于外部而使用的,我们称这种类为内部类。
内部类是依赖于外部类而存在的,一个类是没法谈内外之说的。
2. 内部类简介
内用外,随意访问;外用内,需要内部类对象。
内部类一个特点就是能够访问外部类所有字段(即包括了 private 权限的)。同时,外部类也可以访问内部类的所有访问权限修饰的字段。
就拿人体来说,你能够很明显的感受到你的心脏❤还不是还在跳,心脏也知道你是否还活着。人没了,心脏也就休息了;心脏跳不动了,人也凉了。
内部类的共性
(1). 内部类虽然在类的内部定义,但是是一个独立的类,在编译之后内部类会被编译成独立的.class文件,但是前面冠以外部类的类名和$符号 。
(2). 内部类也需要实例化才能访问
(3). 内部类声明成静态的,就不能随便访问外部类的成员变量了,此时内部类只能访问外部类的静态成员变量 。
(4). 外部类不能直接访问内部类的的成员,但可以通过内部类对象来访问。内部类是外部类的一个成员,因此内部类可以自由地访问外部类的成员变量,无论是否是private的。(后面有解释,详细的源码编译解释参考文末链接文章)
3. 内部类的分类
I. 成员内部类
就是在一个类的内部再定义一个类,成员内部类不能含有static的变量和方法。
如何使用成员内部类,有两种方式:
直接访问
外部类名称.内部类名称 对象名 = new 外部类名称().new 内部类名称();
直接创建内部类对象来访问内部类
public class BodyII { private String name; //成员内部类 public class Heart { //内部类方法 public void beat() { System.out.println("内部类方法"); } } //外部类的方法 public void breathe() { new Heart().beat(); System.out.println("外部类方法"); } public static void main(String[] args) { //直接创建内部类对象来访问 //外部类名称.内部类名称 对象名 = new 外部类名称().new 内部类名称(); BodyII.Heart body = new BodyII().new Heart(); body.beat(); } }
内部类方法
间接访问
在外部类的方法当中,使用内部类;借助外部类的方法间接调用内部类方法
public class BodyI { private String name; //成员内部类 public class Heart { //内部类方法 public void beat() { System.out.println("内部类方法"); } } //外部类的方法 public void breathe() { new Heart().beat(); System.out.println("外部类方法"); } public static void main(String[] args) { BodyI body = new BodyI(); //通过外部类的对象,调用外部类的方法,里面间接在使用内部类 Heart body.breathe(); } }
内部类方法
外部类方法
内部类重名变量的访问
在有内部类的类中,成员变量重名可能会出现
外部类的成员变量
内部类的成员变量
局部变量
在内部类中,访问外部类的重名成员变量时:
外部类名称.this.外部类成员变量名
public class Outer { //外部类名称.this.外部类成员变量名 int num = 10; public class Inner { int num = 20; public void innerMethod() { int num = 30; System.out.println(num); //局部变量,就近原则 System.out.println(this.num); //内部类的成员变量 System.out.println(Outer.this.num); //外部类的成员变量 } } public static void main(String[] args) { Outer.Inner obj = new Outer().new Inner(); obj.innerMethod(); } }
运行结果
30
20
10
II. 静态内部类
我们知道,一个类的静态成员独立于这个类的任何一个对象存在,只要有访问权限,我们就可以通过 类名.静态成员名 来访问这个静态成员,不需要通过实例化对象来调用。
同样的,静态内部类也是作为一个外部类的静态成员而存在,创建一个类的静态内部类对象不需要依赖其外部类对象。
先来看个简单的例子:
运行结果
访问外部类静态成员变量 Hello World
【内部类代码总结】
class Out { private static int i = 10; private int j = 20; public static void outer_f1() { System.out.println("访问外部类的静态成员"); } public void outer_f2() { System.err.println("不能访问外部类的非静态成员"); } //静态内部类 private static class Inner { static int inner_i = 100; int inner_j = 200; // 静态内部类只能访问外部类的静态成员(包括静态变量和静态方法) static void inner_f1() { System.out.println("Outer.i: " + i); outer_f1(); } void inner_f2() { // 静态内部类不能访问外部类的非静态成员(包括非静态变量和非静态方法) } } public void outer_f3() { // 外部类访问内部类的静态成员:内部类.静态成员 System.out.println("Inner.i: " + Inner.inner_i); Inner.inner_f1(); // 外部类访问内部类的非静态成员:实例化内部类即可 new Inner().inner_f2(); } } public class Outerclass { public static void main(String[] args) { new Out().outer_f3(); } }
运行结果
Inner.i: 100
Outer.i: 10
访问外部类的静态成员
可以看到,我们可以把静态内部类当做外部类的一个静态成员,在创建其对象无需依赖外部类对象(访问一个类的静态成员无需依赖这个类的对象,因为它是独立于所有类的对象的)
但是,静态内部类中也无法访问外部类的非静态成员,因为外部类的非静态成员是属于每一个外部类对象的,而本身静态内部类就是独立外部类对象存在的,所以静态内部类不能访问外部类的非静态成员,而外部类依然可以访问静态内部类对象的所有访问权限的成员,这一点和普通内部类无异。
总结
静态内部类只能访问外部类静态成员,不能访问普通成员
外部类可以访问内部类的静态成员:内部类.静态成员
外部类访问内部类的非静态成员:实例化内部类即可
III. 局部内部类
局部内部类,可以类比局部方法、局部变量,都是定义在方法中的,那么局部内部类也就是定义在方法中了。
如果一个类是定义在一个方法内部的,那么这就是一个局部内部类。
局部的含义:只有当前所属的方法才能使用它,出了这个方法外面就不能用了。
public class Outer { public void methodOuter() { class Inner { //局部内部类 int num = 10; public void methodInner() { System.out.println("调用局部内部类:" + num); } } Inner in = new Inner(); in.methodInner(); } public static void main(String[] args) { Outer out = new Outer(); out.methodOuter(); } }
运行结果:
调用局部内部类:10
内部类的权限修饰符
权限修饰符:public > protected > (default) > private
定义内部类时,权限修饰符规则:
外部类:public / (default)
成员内部类:public > protected > (default) > private
局部内部类:(default)默认不写即可
局部内部类的final问题
局部內部类,如果希望访问所在方法的局部变量,那么这个局部变量必须是【有效 final的】
什么意思内呢为什么成员变量num必须得被final修饰呢?
答:因为生命周期的问题,要保证在成员变量num销毁之后,内部类的对象依然可以使用num,需要将num从栈内存拷贝一份到常量池中。
new出来的对象在堆内存当中
局部变量是跟着方法走的,在栈内存当中
方法运行结束之后,立刻出栈,局部变量就会立刻消失
但是new出来的对象会在堆当中持续存在,直到垃圾回收消失
IV. 匿名内部类
如果接囗的实现类(或者是子类)只需要使用唯一的一次,那么这种情况下就可以省略掉该类的定义,而改为使用【匿名內部类】
匿名内部类定义:
对格式new 接口名称(){ };进行解析:
new代表创建对象的动作
接口名称就是匿名内部类需要实现的接口
{ };这才是匿名内部类的内容
匿名内部类,其实就是没有类名的局部内部类
匿名内部类的使用
匿名内部类的使用是很频繁的,最常用的就是在接口的实现上。
通常情况下,我们在再实用接口时,会先定义接口,然后是实现接口,最后是实例化实现类来使用。
比如说一个登陆功能,我们通常会这样写:
public interface Userdao { void login(User user); //用户登录 } public class UserdaoImpl implements UserDao { public void login(User user); { System.out.println("查询用户记录"); } } ...... Userdao dao = new UserDaoImpl(); dao.login();
像这样写每个功能模块是独立的,代码的逻辑结构更加清晰,方便我们多次使用接口和实现类。
但是,有的时候我们的实现类只会使用一次,像上面定义又比较繁琐,必须得有接口的实现类才可以实例化来使用,这时我们可以使用匿名内部类。
【举例来说】
在我们定义实现链表时,我们会写一个链表的迭代器来遍历链表的每一个节点,就要实现Iterable接口
但是,我们对这个类只使用一次,最终能够迭代遍历链表即可,所以用匿名内部类得方式实现Iterable接口。
public class LinkList<T> implements Iterable<T> { ...... //链表功能实现 /** * 实例化Iterator对象,Iterator是接口,用内部类来实例化 * @return */ @Override public Iterator iterator() { return new Iterator() { private Node node = head; @Override public boolean hasNext() { return node.next != null; } @Override public Object next() { //指向第一个存储数据的节点 node = node.next; return node.data; } }; } }
那匿名内部类好像写的很隐秘,不太容易看出来。其实,鉴别的方法很简单,如果代码块有;,那就要好好判断一下了。
{ };
匿名内部类小结
匿名对象:
匿名内部类实现了方法. 匿名对象来调用方法!
匿名内部类,在【创建对象】的时候,只能使用唯一一次。
如果希望多次创建对象,而且类的内容一样的话,那么就必须使用单独定义的实现类了
匿名对象,在【调用方法】的时候,只能调用唯一一次。
如果希望同一个对象,调用多次方法,那么必须给对象起个名字。
匿名内部类是省略了【实现类/子类名称】,但是匿名对象是省略了【对象名称】
强调:匿名内部类和匿名对象不是一回事!!!
Q:为什么内部类可以访问外部类的成员
深入理解Java中为什么内部类可以访问外部类的成员
编译器自动为内部类添加一个成员变量, 这个成员变量的类型和外部类的类型相同, 这个成员变量就是指向外部类对象的引用;
编译器自动为内部类的构造方法添加一个参数, 参数的类型是外部类的类型, 在构造方法内部使用这个参数为添加的成员变量赋值;
在调用内部类的构造函数初始化内部类对象时, 会默认传入外部类的引用
Q:为什么成员内部类不能含有static的变量和方法?
其实还是类与对象的区别,静态属性不依赖于对象,所以访问修改的时候不需要依赖当前有没有存活的对象。
内部类可以认为是外部类的一个成员变量,想要访问内部类,必须先实例化外部类,然后通过外部类才能访问内部类。只要是成员变量,就需要依赖具体的对象,这个时候如果在非静态内部类里面声明静态属性就会破坏了这一逻辑,所以java语言在语义层面不允许我们那么做。
看完有什么不懂的可以在下方留言评论,记得点个赞哦