Java常见问题总结

6 篇文章 0 订阅

May you return with a young heart after years of fighting. ​​​
愿你出走半生,归来仍是少年。

1.hashCode方法与equals方法

java中对象的存储采用哈希表的存储方式,hashcode方法是根据对象的地址转换之后返回的一个哈希值。看哈希表的结构:
哈希表
对于hashcode方法,会返回一个哈希值,哈希值对数组的长度(2*31-1)取余后会确定一个存储的下标位置,即数组位置。所以哈希值相同或者取余相同在一个链表上。
不同的哈希值取余之后的结果相同,用equals方法判断是否为相同的对象,不同则在链表中插入。

为什么重写hashCode必须重写equals方法?

如果只重写了equals方法,而不重写hashcode的方法,会造成hashcode的值不同(不同对象),而equals()方法判断出来的结果为true,所以重写equals方法必须重写hashCode。
进一步说明:
在Java中的一些容器中,不允许有两个完全相同的对象,插入的时候,如果判断相同则会进行覆盖。这时候如果只重写了equals()的方法,而不重写hashcode的方法,Object中hashcode是根据对象的存储地址转换而形成的一个哈希值。这时候就有可能因为没有重写hashcode方法,造成相同的对象散列到不同的位置而造成对象的不能覆盖的问题。

面试总结:
1.hashcode不同(地址不同,不在一个链上),equals()方法判断的返回的一定为false(肯定不同)。
2.hashcode相同,equals()方法返回值不能确认,可能为true,可能为false。
3.重写equals方法必须重写hashCode。

2.静态内部类和非静态内部类的区别

2.1. 静态内部类不持有外部类的引用
在普通内部类中,可以直接访问外部类的属性、方法(包括private类型),这是因为内部类持有外部类的引用,可以自由访问。而静态内部类只能访问外部类的静态方法和静态属性(包括private类型)。

2. 静态内部类不依赖外部类
普通内部类与外部类之间是相互依赖关系,内部类实例不能脱离外部类实例,也就是说他们会同生共死一起声明,一起被垃圾回收。而静态内部类是可以独立存在的,即使外部类消亡了,静态内部类还是可以存在的。静态内部类形似内部,神似外部(类似普通类)。编译之后的类文件名格式为:外部类$内部类。

3. 普通内部类不能声明static的方法和变量
普通内部类不能声明static的方法和变量,允许static常量;而静态内部类形似外部类,没有任何限制。

3.java的类加载机制

类的加载指:将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构。

JVM把描述类的数据从Class文件加载到内存,并对数据校验转换解析初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的加载机制。
类从被加载到虚拟机内存中开始,到卸载出内存为止,整个生命周期(加载过程):
类生命周期

其他:
类加载器(ClassLoader):负责将.class文件加载到内存中,并为之生成对应的class对象。
类加载器组成:
根类加载器:又叫启动类加载器,负责java的核心类加载:System、String
扩展类加载器:负责jre在扩展目录中jar包加载。(jdk/jre/lib/ext目录下的jar包和class文件)
系统类加载器:又叫应用类加载器,负责在JVM启动时加载来自java命令的class文件以及classpath环境变量所指定的jar包和类路径。

类卸载条件
1.该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。
2.加载该类的ClassLoader已经被回收。
3.该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。
如果以上三个条件全部满足,jvm就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程其实就是在方法区中清空类信息,java类的整个生命周期就结束了。

扩展:
java双亲委派机制及作用
概念:当某个类加载器需要加载某个.class文件时,首先检查这个class是否已经加载过了,否则把这个任务委托给他的上级类(父级)加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。

protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException{
        synchronized (getClassLoadingLock(name)) {
            // 首先检查这个classsh是否已经加载过了
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    // 没有加载,如果有父类的加载器则让父类加载器加载
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        //如果父类的加载器为空 则说明递归到bootStrapClassloader了
                        //bootStrapClassloader比较特殊无法通过get获取
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {}
                if (c == null) {
                    //如果bootstrapClassLoader 仍然没有加载过,则递归回来,尝试自己去加载class
                    long t1 = System.nanoTime();
                    c = findClass(name);
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

双亲委派机制的作用
1.防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
2、保证核心.class不能被篡改。通过委托方式,不会去篡改核心.clas,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。

4.Serializable与Parcelable问题

序列化、反序列化是什么?

序列化本质就是把对象内存中的数据按照一定规则,变成一系列的字节数据(因为java里面采用的是Unicode 编码的 16位),然后再把这些字节数据写入流中。而反序列化的过程刚好相反,先读取字节数据,然后再重新组装成java对象。(对象<——>字节数据)

Serializable是Java自带的,而Parcelable是安卓专有的,两者最大的区别在于存储媒介的不同

SerializableParcelable
实现简单实现相对复杂
硬盘中读写内存中读写
效率低效率高

从性能角度考虑,Android中建议使用Parcelable。

5.构造函数的继承及Implicit super constructor XXX() is undefined. Must explicitly invoke another construcor错误

子类继承父类,在new的时候,会默认调用父类的无参构造函数,
这里父类中已经有一个有参构造,于是系统不会默认生成无参构造函数,那么在子类new的时候,找不到父类无参构造函数,就出现了错误
解决办法1:父类添加无参构造;2.在子类有参构造调用父类的有参构造。

6.Java接口与抽象类的区别

Java提供和支持创建抽象类和接口。它们的实现有共同点,不同点在于:

1.接口中所有的方法隐含的都是抽象的。而抽象类则可以同时包含抽象和非抽象的方法。
2.类可以实现很多个接口,但是只能继承一个抽象类
3.类可以不实现抽象类和接口声明的所有方法,当然,在这种情况下,类也必须得声明成是抽象的。
4.抽象类可以在不提供接口方法实现的情况下实现接口。
5.Java接口中声明的变量默认都是final的。抽象类可以包含非final的变量。
6.Java接口中的成员函数默认是public的。抽象类的成员函数可以是private,protected或者是public。
7.接口是绝对抽象的,不可以被实例化,抽象类也不可以被实例化。
8.一个类实现接口的话要实现接口的所有方法,而抽象类不一定。
一句话总结:
从设计层面来说,抽象是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。

7.Java静态代码块何时调用

静态代码块在类加载时调用,并且只调用一次
1.当调用一个类的静态变量时,这个类中的静态代码块会执行。【只有静态代码块会执行】
2.当调用一个 类的静态方法时,这个类中的静态代码块会执行。【只有静态代码块会执行】
3.当创建一个 类的一个实例时,这个类中的静态代码块、非静态代码块(也叫构造代码块)、创建实例的相应的构造方法都会执行。总之,静态代码块是跟随类的,类一动就会执行静态代码块。
两个具有继承关系类的初始化顺序
父类的(静态变量、静态初始化块)=> 子类的(静态变量、静态初始化块)=> 父类的(变量、初始化块、构造器)=> 子类的(变量、初始化块、构造器)

注意:必须把静态变量定义在静态代码块的前面, 因为两个的执行是会根据代码编写的顺序来决定的。

8.static生命周期

加载:java虚拟机在加载类的过程中为静态变量分配内存。
类变量:static变量在内存中只有一个,存放在方法区,属于类变量,被所有实例所共享
销毁:类被卸载时,静态变量被销毁,并释放内存空间。static变量的生命周期取决于类的生命周期。

类初始化顺序:
静态变量、静态代码块初始化-构造函数

结论:想要用static存一个变量,使得下次程序运行时还能使用上次的值是不可行的。因为静态变量生命周期虽然长(就是类的生命周期),但是当程序执行完,也就是该类的所有对象都已经被回收,或者加载类的ClassLoader已经被回收,那么该类就会从jvm的方法区卸载,即生命期终止。

static变量终究是存在jvm的内存中的,jvm下次重新运行时,会清空里边上次运行的内容,包括方法区、常量区的内容。要实现某些变量在程序多次运行时都可以读取,可以采用文件存储等方式实现持久化。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值