• Java基础面试


    文章目录

    Java 概述

    1.什么是Java

    Java是一门面向对象编程语言,不仅吸收了C++语言的各种优点,还摒弃了C+里难以理解的多继承、指针等概念,因此Java语言具有功能强大和简单易用两个特征。Java语言作为静态面向对象编程语言的代表,极好地实现了面向对象理论,允许程序员以优雅的思维方式进行复杂的编程。

    2. JDK1.5 之后的三大版本

    • Java SE (J2SE,Java 2 Platform Standard Edition,标准版):Java SE以前称为J2SE。它允许开发和部署在桌面、服务器、嵌入式环境和实时环境中使用的Java应用程序。Java SE包含了支持Java Web服务开发的类,并为JavaEE和Java ME提供基础.
    • Java EE (J2EE,Java 2 Platform Enterprise Edition,企业版):Java EE以前称为J2EE。企业版本帮助开发和部署可移植、健壮、可伸缩且安全的服务器端Java应用程序。Java EE是在Java SE的基础上构建的,它提供Web服务、组件模型、管理和通信API,可以用来实现企业级的面向服务体系结构(service-oriented architecture,SOA)和Web2.0应用程序。2018年2月,Eclipse宣布正式将JavaEE更名为JakartaEE
    • Java ME (J2ME,Java 2 Platform Micro Edition,微型版):Java ME以前称为J2ME。Java ME为在移动设备和嵌入式设备(比如手机、PDA、电视机打印机)上运行的应用程序提供一个健壮且灵活的环境。Java ME包括灵活的用户界面、健壮的安全模型、许多内置的网络协议以及对可以动态下载的连网和离线应用程序的丰富支持。基于JavaME规范的应用程序只需编写一次,就可以用于许多设备,而且可以利用每个设备的本机功能。

    3. Jdk和Jre和JVM的区别

    在这里插入图片描述

    4. 什么是跨平台性?原理是什么

    • 所谓跨平台性,是指java语言编写的程序,一次编译后,可以在多个系统平台上运行。
    • 实现原理:Java程序是通过java虚拟机在系统平台上运行的,只要该系统可以安装相应的java虚拟机,该系统就可以运行java程序。

    5. Java 语言有哪些特点?

    1. 简单易学;
    2. 面向对象(封装,继承,多态);
    3. 平台无关性( Java 虚拟机实现平台无关性);
    4. 支持多线程( C++ 11开始(2011年的时候),C++才引入了多线程库);
    5. 可靠性;
    6. 安全性;
    7. 支持网络编程并且很方便( Java 语言诞生本身就是为简化网络编程设计的,因此 Java 语言不仅支持网络编程而且很方便);
    8. 编译与解释并存;

    6. 什么是字节码?采用字节码的最大好处是什么

    字节码: Java源代码经过javac编译器编译后产生的文件(即扩展为.class的文件),它不面向任何特定的处理器,只面向虚拟机。

    采用字节码的好处: Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以java程序运行时比较高效,而且,由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。
    在这里插入图片描述
    我们需要格外注意的是 .class->机器码 这一步。在这一步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 JIT(just-in-time compilation) 编译器,而 JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定是高于 Java 解释器的。这也解释了我们为什么经常会说 Java 是编译与解释共存的语言 。

    HotSpot 采用了惰性评估(Lazy Evaluation)的做法,根据二八定律,消耗大部分系统资源的只有那一小部分的代码(热点代码),而这也就是 JIT 所需要编译的部分。JVM 会根据代码每次被执行的情况收集信息并相应地做出一些优化,因此执行的次数越多,它的速度就越快。JDK 9 引入了一种新的编译模式 AOT(Ahead of Time Compilation),它是直接将字节码编译成机器码,这样就避免了 JIT 预热等各方面的开销。JDK 支持分层编译和 AOT 协作使用。

    7. 为什么不全部使用 AOT(since JDK9) 呢?

    AOT 可以提前编译节省启动时间,那为什么不全部使用这种编译方式呢?

    长话短说,这和 Java 语言的动态特性有千丝万缕的联系了。举个例子,CGLIB 动态代理使用的是 ASM 技术,而这种技术大致原理是运行时直接在内存中生成并加载修改后的字节码文件也就是 .class 文件,如果全部使用 AOT 提前编译,也就不能使用 ASM 技术了。为了支持类似的动态特性,所以选择使用 JIT 即时编译器。

    8. 为什么说 Java 语言“编译与解释并存”?

    我们可以将高级编程语言按照程序的执行方式分为两种:

    • 编译型 :编译型语言 会通过编译器将源代码一次性翻译成可被该平台执行的机器码。一般情况下,编译语言的执行速度比较快,开发效率比较低。常见的编译性语言有 C、C++、Go、Rust 等等。
    • 解释型 :解释型语言会通过解释器一句一句的将代码解释(interpret)为机器代码后再执行。解释型语言开发效率比较快,执行速度比较慢。常见的解释性语言有 Python、JavaScript、PHP 等等。
      在这里插入图片描述

    为了改善编译语言的效率而发展出的即时编译技术,已经缩小了这两种语言间的差距。这种技术混合了编译语言与解释型语言的优点,它像编译语言一样,先把程序源代码编译成字节码。到执行期时,再将字节码直译,之后执行。Java与LLVM是这种技术的代表产物。
    相关阅读:基本功 | Java 即时编译器原理解析及实践

    为什么说 Java 语言“编译与解释并存”?
    这是因为 Java 语言既具有编译型语言的特征,也具有解释型语言的特征。因为 Java 程序要经过先编译,后解释两个步骤,由 Java 编写的程序需要先经过编译步骤,生成字节码(.class 文件),这种字节码必须由 Java 解释器来解释执行。
    ##9. Oracle JDK vs OpenJDK
    原文连接:https://javaguide.cn/java/basis/java-basic-questions-01.html#oracle-jdk-vs-openjdk

    数据类型

    1. Java有哪些数据类型

    1. Java基本数据类型
      在这里插入图片描述
      这八种基本类型都有对应的包装类分别为:Byte、Short、Integer、Long、Float、Double、Character、Boolean 。

    2. 引用数据类型
      引用数据类型非常多,大致包括:类、 接口类型、 数组类型、 枚举类型、 注解类型、 字符串型
      例如,String 类型就是引用类型、八种基本类型的包装类。简单来说,所有的非基本数据类型都是引用数据类型。

    2. 字符型常量和字符串常量的区别?

    1. 形式 : 字符常量是单引号引起的一个字符,字符串常量是双引号引起的 0 个或若干个字符。
    2. 含义 : 字符常量相当于一个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表一个地址值(该字符串在内存中存放位置)。
    3. 占内存大小 : 字符常量只占 2 个字节; 字符串常量占若干个字节。
      (注意: char 在 Java 中占两个字节)

    3. float f=3.4 是否正确

    不正确。3.4是双精度数,将双精度型(double)赋值给浮点型(float)属于下转型(down-casting,也称为窄化)会造成精度损失,因此需要强制类型转换float f=(float)3.4;或者写成floatf =3.4F;

    4. 基本类型和包装类型的区别?

    • 成员变量包装类型不赋值就是 null ,而基本类型有默认值且不是 null。
    • 包装类型可用于泛型,而基本类型不可以。
    • 基本数据类型的局部变量存放在 Java 虚拟机栈中的局部变量表中,基本数据类型的成员变量(未被 static 修饰 )存放在 Java 虚拟机的堆中。包装类型属于对象类型,我们知道几乎所有对象实例都存在于堆中。
    • 相比于包装类型, 基本数据类型占用的空间非常小。

    为什么说是几乎所有对象实例呢?
    这是因为 HotSpot 虚拟机引入了 JIT 优化之后,会对对象进行逃逸分析,如果发现某一个对象并没有逃逸到方法外部,那么就可能通过标量替换来实现栈上分配,而避免堆上分配内存

    5. 包装类型的缓存机制了解么?

    Java 基本数据类型的包装类型的大部分都用到了缓存机制来提升性能,其中Byte,Short,Integer,Long 这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character 创建了数值在 [0,127] 范围的缓存数据,Boolean 直接返回 True or False。

    Integer 缓存源码:

    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }
    
    private static class IntegerCache {
        static final int low = -128;
        static final int high;
        static {
            // high value may be configured by property
            int h = 127;
            ...
        }
        .......
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    Character 缓存源码:

    public static Character valueOf(char c) {
        if (c <= 127) { // must cache
          return CharacterCache.cache[(int)c];
        }
        return new Character(c);
    }
    
    private static class CharacterCache {
        private CharacterCache(){}
        static final Character cache[] = new Character[127 + 1];
        static {
            for (int i = 0; i < cache.length; i++)
                cache[i] = new Character((char)i);
        }
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    Boolean 缓存源码:

    public static Boolean valueOf(boolean b) {
        return (b ? TRUE : FALSE);
    }
    
    
    • 1
    • 2
    • 3
    • 4

    注意: 如果超出对应范围仍然会去创建新的对象,缓存的范围区间的大小只是在性能和资源之间的权衡。

    下面我们来看一下问题。下面的代码的输出结果是 true 还是 false 呢?

    Integer i1 = 40;
    Integer i2 = new Integer(40);
    System.out.println(i1 == i2);
    
    
    • 1
    • 2
    • 3
    • 4

    解释: Integer i1=40 这一行代码会发生装箱,也就是说这行代码等价于 Integer i1=Integer.valueOf(40) 。因此,i1 直接使用的是缓存中的对象。而Integer i2 = new Integer(40) 会直接创建新的对象。因此,答案是 false 。你答对了吗?

    注意:所有整型包装类对象之间值的比较,全部使用 equals 方法比较。

    在这里插入图片描述

    6. 自动装箱与拆箱了解吗?原理是什么?

    什么是自动拆装箱?

    • 装箱:将基本类型用它们对应的引用类型包装起来;
    • 拆箱:将包装类型转换为基本数据类型;
    Integer i = 10;  //装箱
    int n = i;   //拆箱
    
    
    • 1
    • 2
    • 3

    7. 为什么浮点数运算的时候会有精度丢失的风险?

    浮点数运算精度丢失代码演示:

    float a = 2.0f - 1.9f;
    float b = 1.8f - 1.7f;
    System.out.println(a);// 0.100000024
    System.out.println(b);// 0.099999905
    System.out.println(a == b);// false
    
    • 1
    • 2
    • 3
    • 4
    • 5

    为什么会出现这个问题呢?

    这个和计算机保存浮点数的机制有很大关系。我们知道计算机是二进制的,而且计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。这也就是解释了为什么浮点数没有办法用二进制精确表示。

    8. 如何解决浮点数运算的精度丢失问题?

    BigDecimal 可以实现对浮点数的运算,不会造成精度丢失。通常情况下,大部分需要浮点数精确运算结果的业务场景(比如涉及到钱的场景)都是通过 BigDecimal 来做的。

    BigDecimal a = new BigDecimal("1.0");
    BigDecimal b = new BigDecimal("0.9");
    BigDecimal c = new BigDecimal("0.8");
    
    BigDecimal x = a.subtract(b);
    BigDecimal y = b.subtract(c);
    
    System.out.println(x); /* 0.1 */
    System.out.println(y); /* 0.1 */
    System.out.println(Objects.equals(x, y)); /* true */
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    BigDecimal 详解

    9. 超过 long 整型的数据应该如何表示?

    基本数值类型都有一个表达范围,如果超过这个范围就会有数值溢出的风险。

    在 Java 中,64 位 long 整型是最大的整数类型。

    long l = Long.MAX_VALUE;
    System.out.println(l + 1); // -9223372036854775808
    System.out.println(l + 1 == Long.MIN_VALUE); // true
    
    
    • 1
    • 2
    • 3
    • 4

    BigInteger 内部使用 int[] 数组来存储任意大小的整形数据。

    相对于常规整数类型的运算来说,BigInteger 运算的效率会相对较低。

    基础语法

    1. 标识符和关键字的区别是什么?

    在我们编写程序的时候,需要大量地为程序、类、变量、方法等取名字,于是就有了 标识符 。简单来说, 标识符就是一个名字 。

    有一些标识符,Java 语言已经赋予了其特殊的含义,只能用于特定的地方,这些特殊的标识符就是 关键字简单来说,关键字是被赋予特殊含义的标识符 。比如,在我们的日常生活中,如果我们想要开一家店,则要给这个店起一个名字,起的这个“名字”就叫标识符。但是我们店的名字不能叫“警察局”,因为“警察局”这个名字已经被赋予了特殊的含义,而“警察局”就是我们日常生活中的关键字。

    2. continue、break 和 return 的区别是什么?

    在循环结构中,当循环条件不满足或者循环次数达到要求时,循环会正常结束。但是,有时候可能需要在循环的过程中,当发生了某种条件之后 ,提前终止循环,这就需要用到下面几个关键词:

    • continue :指跳出当前的这一次循环,继续下一次循环。
    • break :指跳出整个循环体,继续执行循环下面的语句。
    • return 用于跳出所在方法,结束该方法的运行。return 一般有两种用法:
      • return; :直接使用 return 结束方法执行,用于没有返回值函数的方法
      • return value; :return 一个特定值,用于有返回值函数的方法

    3. Java语言关键字有哪些?

    在这里插入图片描述
    官方文档:https://docs.oracle.com/javase/tutorial/java/nutsandbolts/_keywords.html

    4. switch 是否能作用在 byte 上,是否能作用在 long 上,是否能作用在 String 上?

    在Java 5以前,switch(expr)中,expr只能是byte、short、char、int。从Java5开始,Java中引入了枚举类型,expr也可以是enum类型,从Java 7开始,expr还可以是字符串(String),但是长整型(long)在目前所有的版本中都是不可以的。

    5. Math.round(11.5) 等于多少?Math.round(-11.5)等于多少?

    Math.round(11.5)的返回值是 12,Math.round(-11.5)的返回值是-11。四舍五入的原理是在参数上加 0.5 然后进行下取整。

    6. 成员变量与局部变量的区别?

    • 语法形式 : 从语法形式上看,成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。
    • 存储方式 : 从变量在内存中的存储方式来看,如果成员变量是使用 static 修饰的,那么这个成员变量是属于类的,如果没有使用 static 修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。
    • 生存时间 : 从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动生成,随着方法的调用结束而消亡。
    • 默认值 : 从变量是否有默认值来看,成员变量如果没有被赋初始值,则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。

    7. 静态方法为什么不能调用非静态成员?

    1. 静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问。而非静态成员属于实例对象,只有在对象实例化之后才存在,需要通过类的实例对象去访问。
    2. 在类的非静态成员不存在的时候静态成员就已经存在了,此时调用在内存中还不存在的非静态成员,属于非法操作。

    8. 什么是可变长参数?

    从 Java5 开始,Java 支持定义可变长参数,所谓可变长参数就是允许在调用方法时传入不定长度的参数。就比如下面的这个 printVariable 方法就可以接受 0 个或者多个参数。

    public static void printVariable (String... args) {
       //......
    }
    
    
    • 1
    • 2
    • 3
    • 4

    另外,可变参数只能作为函数的最后一个参数,但其前面可以有也可以没有任何其他参数。

    public static void printVariable1(String arg1, String... args) {
       //......
    }
    
    
    • 1
    • 2
    • 3
    • 4

    9. 遇到方法重载的情况怎么办呢?会优先匹配固定参数还是可变参数的方法呢?

    答案是会优先匹配固定参数的方法,因为固定参数的方法匹配度更高

    10. 访问修饰符 public,private,protected,以及不写(默认)时的区别

    在这里插入图片描述

    面向对象基础

    1. 面向对象和面向过程的区别

    两者的主要区别在于解决问题的方式不同:

    • 面向过程把解决问题的过程拆成一个个方法,通过一个个方法的执行解决问题。
    • 面向对象会先抽象出对象,然后用对象执行方法的方式解决问题。
    • 另外,面向对象开发的程序一般更易维护、易复用、易扩展。

    2. 成员变量与局部变量的区别

    • 语法形式 : 从语法形式上看,成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。
    • 存储方式 : 从变量在内存中的存储方式来看,如果成员变量是使用 static 修饰的,那么这个成员变量是属于类的,如果没有使用 static 修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。
    • 生存时间 : 从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动生成,随着方法的调用结束而消亡。
    • 默认值 : 从变量是否有默认值来看,成员变量如果没有被赋初始值,则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。

    3. 创建一个对象用什么运算符?对象实体与对象引用有何不同?

    new 运算符,new 创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。

    一个对象引用可以指向 0 个或 1 个对象(一根绳子可以不系气球,也可以系一个气球);一个对象可以有 n 个引用指向它(可以用 n 条绳子系住一个气球)。

    4. 对象的相等和引用相等的区别

    • 对象的相等一般比较的是内存中存放的内容是否相等。
    • 引用相等一般比较的是他们指向的内存地址是否相等

    5. 类的构造方法的作用是什么?

    构造方法是一种特殊的方法,主要作用是完成对象的初始化工作。

    6. 如果一个类没有声明构造方法,该程序能正确执行吗?

    如果一个类没有声明构造方法,也可以执行!因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。如果我们自己添加了类的构造方法(无论是否有参),Java 就不会再添加默认的无参数的构造方法了,我们一直在不知不觉地使用构造方法,这也是为什么我们在创建对象的时候后面要加一个括号(因为要调用无参的构造方法)。如果我们重载了有参的构造方法,记得都要把无参的构造方法也写出来(无论是否用到),因为这可以帮助我们在创建对象的时候少踩坑。

    7. 构造方法有哪些特点?是否可被 override?

    构造方法特点如下:

    • 名字与类名相同。
    • 没有返回值,但不能用 void 声明构造函数。
    • 生成类的对象时自动执行,无需调用。

    构造方法不能被 override(重写),但是可以 overload(重载),所以你可以看到一个类中有多个构造函数的情况。

    8. 面向对象三大特征

    • 封装: 隐藏部分对象的属性和实现细节,对数据的访问只能通过外公开的接口。通过这种方式,对象对内部数据提供了不同级别的保护,以防止程序中无关的部分意外的改变或错误的使用了对象的私有部分。
    • 继承: 在已有类的基础上,进行扩展形成新的类,提高代码复用性。继承是多态的前提。子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有。子类可以拥有自己属性和方法,即子类可以对父类进行扩展。子类可以用自己的方式实现父类的方法。(重写)
    • 多态:
      • 多态分为编译时多态和运行时多态: 1. 编译时多态主要指方法的重载。 2. 运行时多态指程序中定义的对象引用所指向的具体类型在运行期间才确定
      • 运行时多态有三个条件: 1. 继承 2. 覆盖(重写) 3. 向上转型(父类引用指向子类对象)

    如下:

    public class Instrument {
        public void play() {
            System.out.println("Instrument is playing...");
        }
    }
    
    public class Wind extends Instrument {
        public void play() {
            System.out.println("Wind is playing...");
        }
    }
    
    public class Percussion extends Instrument {
        public void play() {
            System.out.println("Percussion is playing...");
        }
    }
    
    public class Music {
        public static void main(String[] args) {
            List<Instrument> instruments = new ArrayList<>();
            instruments.add(new Wind());
            instruments.add(new Percussion());
            for(Instrument instrument : instruments) {
                instrument.play();
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    9 接口和抽象类有什么共同点和区别?

    共同点 :

    1. 都不能被实例化。
    2. 都可以包含抽象方法。
    3. 都可以有默认实现的方法(Java 8 可以用 default 关键字在接口中定义默认方法)。

    区别 :

    1. 接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系(比如说我们抽象了一个发送短信的抽象类,)。
    2. 一个类只能继承一个类,但是可以实现多个接口。
    3. 接口中的成员变量只能是 public static final 类型的,不能被修改且必须有初始值,而抽象类的成员变量默认 default,可在子类中被重新定义,也可被重新赋值。

    10. 深拷贝和浅拷贝区别了解吗?什么是引用拷贝?

    关于深拷贝和浅拷贝区别,我这里先给结论:

    • 浅拷贝:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),不过,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象。
    • 深拷贝 :深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。

    那什么是引用拷贝呢? 简单来说,引用拷贝就是两个不同的引用指向同一个对象。
    在这里插入图片描述

    11. 内部类

    1. 内部类

    内部类可以分为四种:成员内部类、局部内部类、匿名内部类和静态内部类
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    2. 内部类的优点

    在这里插入图片描述

    3. 内部类有哪些应用场景

    • 一些多算法场合
    • 解决一些非面向对象的语句块。
    • 适当使用内部类,使得代码更加灵活和富有扩展性。
    • 当某个类除了它的外部类,不再被其他的类使用时。

    4. 局部内部类和匿名内部类访问局部变量的时候,为什么变量必须要加上final?

    在这里插入图片描述

    5. 内部类相关,看程序说出运行结果

    public class Outer {
        private int age = 12;
    
        class Inner {
            private int age = 13;
            public void print() {
                int age = 14;
                System.out.println(age);
                System.out.println(this.age);
                System.out.println(Outer.this.age);
            }
        }
    
        public static void main(String[] args) {
            Outer.Inner in = new Outer().new Inner();
            in.print();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    结果:

    14
    13
    12
    
    • 1
    • 2
    • 3

    12. hashcode

    1. hashcode()介绍

    hashCode()的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode()定义在JDK的Object.java中,这就意味着Java中的任何类都包含有hashCode()函数。散列表存储的是键值对(key-value),它的特点是:能根据′“键"快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)

    2. 为什么要有hashCode?

    我们以"Hashset 如何检查重复"为例子来说明为什么要有 hashcode:

    当你把对象加入HashSet时,HashSet 会先计算对象的 hashcode值来判断对象加入的位置,同时也会与其他已经加入的对象的hashcode值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同hashcode值的对象,这时会调用equals()方法来检查hashcode相等的对象是否真的相同。如果两者相同,HashSet就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。(摘自我的Java启蒙书《Head first java》第二版)。这样我们就大大减少了equals 的次数,相应就大大提高了执行速度。

    3. 请解释hashCode()和equals()方法有什么联系?

    1. 如果两个对象相等,则hashcode一定也是相同的
    2. 两个对象相等,对两个对象分别调用equals方法都返回true
    3. 两个对象有相同的hashcode值,它们也不一定是相等的

    Object和String

    Object

    Object 类的常见方法有哪些?

    Object 类是一个特殊的类,是所有类的父类。它主要提供了以下 11 个方法:

    /**
     * native 方法,用于返回当前运行时对象的 Class 对象,使用了 final 关键字修饰,故不允许子类重写。
     */
    public final native Class<?> getClass()
    /**
     * native 方法,用于返回对象的哈希码,主要使用在哈希表中,比如 JDK 中的HashMap。
     */
    public native int hashCode()
    /**
     * 用于比较 2 个对象的内存地址是否相等,String 类对该方法进行了重写以用于比较字符串的值是否相等。
     */
    public boolean equals(Object obj)
    /**
     * naitive 方法,用于创建并返回当前对象的一份拷贝。
     */
    protected native Object clone() throws CloneNotSupportedException
    /**
     * 返回类的名字实例的哈希码的 16 进制的字符串。建议 Object 所有的子类都重写这个方法。
     */
    public String toString()
    /**
     * native 方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
     */
    public final native void notify()
    /**
     * native 方法,并且不能重写。跟 notify 一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
     */
    public final native void notifyAll()
    /**
     * native方法,并且不能重写。暂停线程的执行。注意:sleep 方法没有释放锁,而 wait 方法释放了锁 ,timeout 是等待时间。
     */
    public final native void wait(long timeout) throws InterruptedException
    /**
     * 多了 nanos 参数,这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。 所以超时的时间还需要加上 nanos 毫秒。。
     */
    public final void wait(long timeout, int nanos) throws InterruptedException
    /**
     * 跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念
     */
    public final void wait() throws InterruptedException
    /**
     * 实例被垃圾回收器回收的时候触发的操作
     */
    protected void finalize() throws Throwable { }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    == 和 equals() 的区别

    == 对于基本类型和引用类型的作用效果是不同的:

    • 对于基本数据类型来说,== 比较的是值。
    • 对于引用数据类型来说,== 比较的是对象的内存地址。

    因为 Java 只有值传递,所以,对于 == 来说,不管是比较基本数据类型,还是引用数据类型的变量,其本质比较的都是值,只是引用类型变量存的值是对象的地址。

    equals() 方法存在两种使用情况:

    • 类没有重写 equals()方法 :通过equals()比较该类的两个对象时,等价于通过“==”比较这两个对象,使用的默认是 Object类equals()方法。
    • 类重写了 equals()方法 :一般我们都重写 equals()方法来比较两个对象中的属性是否相等;若它们的属性相等,则返回 true(即,认为这两个对象相等)。

    hashCode() 有什么用

    hashCode() 的作用是获取哈希码(int 整数),也称为散列码。这个哈希码的作用是确定该对象在哈希表中的索引位置。

    hashCode()定义在 JDK 的 Object 类中,这就意味着 Java 中的任何类都包含有 hashCode() 函数。另外需要注意的是: Object 的 hashCode() 方法是本地方法,也就是用 C 语言或 C++ 实现的,该方法通常用来将对象的内存地址转换为整数之后返回。

    public native int hashCode();
    
    • 1

    散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)

    为什么要有 hashCode?

    我们以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode?

    当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashCode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashCode 值作比较,如果没有相符的 hashCode,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashCode 值的对象,这时会调用 equals() 方法来检查 hashCode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。

    其实, hashCode() 和 equals()都是用于比较两个对象是否相等。

    为什么 JDK 还要同时提供equals()和hashcode()这两个方法呢?

    这是因为在一些容器(比如 HashMap、HashSet)中,有了 hashCode() 之后,判断元素是否在对应容器中的效率会更高(参考添加元素进HashSet的过程)!

    我们在前面也提到了添加元素进HashSet的过程,如果 HashSet 在对比的时候,同样的 hashCode 有多个对象,它会继续使用 equals() 来判断是否真的相同。也就是说 hashCode 帮助我们大大缩小了查找成本。

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

    因为两个相等的对象的 hashCode 值必须是相等。也就是说如果 equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等。

    如果重写 equals() 时没有重写 hashCode() 方法的话就可能会导致 equals 方法判断是相等的两个对象,hashCode 值却不相等。

    思考 : 重写 equals() 时没有重写 hashCode() 方法的话,使用 HashMap 可能会出现什么问题。

    总结 : equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等。
    两个对象有相同的 hashCode 值,他们也不一定是相等的(哈希碰撞)。

    String

    1. String、StringBuffer、StringBuilder 的区别?

    可变性

    1. String 是不可变的(后面会详细分析原因)。
    2. StringBuilderStringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串,不过没有使用 final 和 private 关键字修饰,最关键的是这个 AbstractStringBuilder 类还提供了很多修改字符串的方法比如 append 方法。
    abstract class AbstractStringBuilder implements Appendable, CharSequence {
        char[] value;
        public AbstractStringBuilder append(String str) {
            if (str == null)
                return appendNull();
            int len = str.length();
            ensureCapacityInternal(count + len);
            str.getChars(0, len, value, count);
            count += len;
            return this;
        }
      	//...
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    线程安全性

    String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。

    性能

    1. 每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。
    2. StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。

    对于三者使用的总结:

    1. 操作少量的数据: 适用 String
    2. 单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder
    3. 多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer

    2. String为什么是不可变的?

    部分源码:

    public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
        private final char value[];
    	//...
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5

    我们知道被 final 关键字修饰的类不能被继承,修饰的方法不能被重写,修饰的变量是基本数据类型则值不能改变,修饰的变量是引用类型则不能再指向其他对象。因此,final 关键字修饰的数组保存字符串并不是 String 不可变的根本原因,因为这个数组保存的字符串是可变的(final 修饰引用类型变量的情况)。

    String 真正不可变有下面几点原因:

    1. 保存字符串的数组被 final 修饰且为私有的,并且String 类没有提供/暴露修改这个字符串的方法。
    2. String 类被 final 修饰导致其不能被继承,进而避免了子类破坏 String 不可变。

    3. Java 9 为何要将 String 的底层实现由 char[] 改成了 byte[] ?

    在 Java 9 之后,String 、StringBuilder 与 StringBuffer 的实现改用 byte 数组存储字符串。

    public final class String implements java.io.Serializable,Comparable<String>, CharSequence {
        // @Stable 注解表示变量最多被修改一次,称为“稳定的”。
        @Stable
        private final byte[] value;
    }
    
    abstract class AbstractStringBuilder implements Appendable, CharSequence {
        byte[] value;
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    新版的 String 其实支持两个编码方案: Latin-1 和 UTF-16。如果字符串中包含的汉字没有超过 Latin-1 可表示范围内的字符,那就会使用 Latin-1 作为编码方案。Latin-1 编码方案下,byte 占一个字节(8 位),char 占用 2 个字节(16),byte 相较 char 节省一半的内存空间。JDK 官方就说了绝大部分字符串对象只包含 Latin-1 可表示的字符。

    如果字符串中包含的汉字超过 Latin-1 可表示范围内的字符,byte 和 char 所占用的空间是一样的。

    4. 字符串拼接用“+” 还是 StringBuilder?

    StringBuilder
    原文:https://javaguide.cn/java/basis/java-basic-questions-02.html#%E5%AD%97%E7%AC%A6%E4%B8%B2%E6%8B%BC%E6%8E%A5%E7%94%A8-%E8%BF%98%E6%98%AF-stringbuilder

    5. String.equals() 和 Object.equals() 有何区别?

    String 中的 equals 方法是被重写过的,比较的是 String 字符串的值是否相等。 Object 的 equals 方法是比较的对象的内存地址。

    6. 字符串常量池的作用了解吗?

    字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。

    // 在堆中创建字符串对象”ab“
    // 将字符串对象”ab“的引用保存在字符串常量池中
    String aa = "ab";
    // 直接返回字符串常量池中字符串对象”ab“的引用
    String bb = "ab";
    System.out.println(aa==bb);// true
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    7. String s1 = new String(“abc”);这句话创建了几个字符串对象?

    会创建 1 或 2 个字符串对象。

    1、如果字符串常量池中不存在字符串对象“abc”的引用,那么会在堆中创建 2 个字符串对象“abc”。

    示例代码(JDK 1.8):

    String s1 = new String("abc");
    
    • 1

    2、如果字符串常量池中已存在字符串对象“abc”的引用,则只会在堆中创建 1 个字符串对象“abc”。

    8. intern 方法有什么作用?

    String.intern() 是一个 native(本地)方法,其作用是将指定的字符串对象的引用保存在字符串常量池中,可以简单分为两种情况:

    • 如果字符串常量池中保存了对应的字符串对象的引用,就直接返回该引用。
    • 如果字符串常量池中没有保存了对应的字符串对象的引用,那就在常量池中创建一个指向该字符串对象的引用并返回。

    示例代码(JDK 1.8) :

    // 在堆中创建字符串对象”Java“
    // 将字符串对象”Java“的引用保存在字符串常量池中
    String s1 = "Java";
    // 直接返回字符串常量池中字符串对象”Java“对应的引用
    String s2 = s1.intern();
    // 会在堆中在单独创建一个字符串对象
    String s3 = new String("Java");
    // 直接返回字符串常量池中字符串对象”Java“对应的引用
    String s4 = s3.intern();
    // s1 和 s2 指向的是堆中的同一个对象
    System.out.println(s1 == s2); // true
    // s3 和 s4 指向的是堆中不同的对象
    System.out.println(s3 == s4); // false
    // s1 和 s4 指向的是堆中的同一个对象
    System.out.println(s1 == s4); //true
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    9. String 类型的变量和常量做“+”运算时发生了什么?

    参数传递

    1. 形参&实参

    方法的定义可能会用到 参数(有参的方法),参数在程序语言中分为:

    • 实参(实际参数) :用于传递给函数/方法的参数,必须有确定的值。
    • 形参(形式参数) :用于定义函数/方法,接收实参,不需要有确定的值。
    String hello = "Hello!";
    // hello 为实参
    sayHello(hello);
    // str 为形参
    void sayHello(String str) {
        System.out.println(str);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    2. 值传递与引用传递

    • 值传递 :方法接收的是实参值的拷贝,会创建副本。
    • 引用传递 :方法接收的直接是实参所引用的对象在堆中的地址,不会创建副本,对形参的修改将影响到实参。

    很多程序设计语言(比如 C++、 Pascal )提供了两种参数传递的方式,不过,在 Java 中只有值传递

    3. 为什么 Java 只有值传递?

    案例1:传递基本类型参数

    代码:

    public static void main(String[] args) {
        int num1 = 10;
        int num2 = 20;
        swap(num1, num2);
        System.out.println("num1 = " + num1);
        System.out.println("num2 = " + num2);
    }
    
    public static void swap(int a, int b) {
        int temp = a;
        a = b;
        b = temp;
        System.out.println("a = " + a);
        System.out.println("b = " + b);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    输出:

    a = 20
    b = 10
    num1 = 10
    num2 = 20
    
    
    • 1
    • 2
    • 3
    • 4
    • 5

    解析:

    在 swap() 方法中,a、b 的值进行交换,并不会影响到 num1、num2。因为,a、b 的值,只是从 num1、num2 的复制过来的。也就是说,a、b 相当于 num1、num2 的副本,副本的内容无论怎么修改,都不会影响到原件本身。

    案例2:传递引用类型参数1

    代码:

    	public static void main(String[] args) {
          int[] arr = { 1, 2, 3, 4, 5 };
          System.out.println(arr[0]);
          change(arr);
          System.out.println(arr[0]);
    	}
    
    	public static void change(int[] array) {
          // 将数组的第一个元素变为0
          array[0] = 0;
    	}
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    输出:

    1
    0
    
    • 1
    • 2

    解析:
    在这里插入图片描述
    看了这个案例很多人肯定觉得 Java 对引用类型的参数采用的是引用传递。

    实际上,并不是的,这里传递的还是值,不过,这个值是实参的地址罢了!

    也就是说 change 方法的参数拷贝的是 arr (实参)的地址,因此,它和 arr 指向的是同一个数组对象。这也就说明了为什么方法内部对形参的修改会影响到实参。

    案例3 :传递引用类型参数2

    public class Person {
        private String name;
       // 省略构造函数、Getter&Setter方法
    }
    
    public static void main(String[] args) {
        Person xiaoZhang = new Person("小张");
        Person xiaoLi = new Person("小李");
        swap(xiaoZhang, xiaoLi);
        System.out.println("xiaoZhang:" + xiaoZhang.getName());
        System.out.println("xiaoLi:" + xiaoLi.getName());
    }
    
    public static void swap(Person person1, Person person2) {
        Person temp = person1;
        person1 = person2;
        person2 = temp;
        System.out.println("person1:" + person1.getName());
        System.out.println("person2:" + person2.getName());
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    输出:

    person1:小李
    person2:小张
    xiaoZhang:小张
    xiaoLi:小李
    
    • 1
    • 2
    • 3
    • 4

    解析:

    怎么回事???两个引用类型的形参互换并没有影响实参啊!

    swap 方法的参数 person1 和 person2 只是拷贝的实参 xiaoZhang 和 xiaoLi 的地址。因此, person1 和 person2 的互换只是拷贝的两个地址的互换罢了,并不会影响到实参 xiaoZhang 和 xiaoLi 。
    在这里插入图片描述

    Java序列化详解

    1. 什么是序列化?什么是反序列化?

    如果我们需要持久化 Java 对象比如将 Java 对象保存在文件中,或者在网络传输 Java 对象,这些场景都需要用到序列化。

    简单来说:

    • 序列化: 将数据结构或对象转换成二进制字节流的过程
    • 反序列化:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程

    维基百科是如是介绍序列化的:

    序列化(serialization)在计算机科学的数据处理中,是指将数据结构或对象状态转换成可取用格式(例如存成文件,存于缓冲,或经由网络中发送),以留待后续在相同或另一台计算机环境中,能恢复原先状态的过程。依照序列化格式重新获取字节的结果时,可以利用它来产生与原始对象相同语义的副本。对于许多对象,像是使用大量引用的复杂对象,这种序列化重建的过程并不容易。面向对象中的对象序列化,并不概括之前原始对象所关系的函数。这种过程也称为对象编组(marshalling)。从一系列字节提取数据结构的反向操作,是反序列化(也称为解编组、deserialization、unmarshalling)。

    综上:序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。

    在这里插入图片描述

    2. 实际开发中有哪些用到序列化和反序列化的场景?

    1. 对象在进行网络传输(比如远程方法调用 RPC 的时候)之前需要先被序列化,接收到序列化的对象之后需要再进行反序列化;
    2. 将对象存储到文件中的时候需要进行序列化,将对象从文件中读取出来需要进行反序列化。
    3. 将对象存储到缓存数据库(如 Redis)时需要用到序列化,将对象从缓存数据库中读取出来需要反序列化。

    3. 序列化协议对应于 TCP/IP 4 层模型的哪一层?

    我们知道网络通信的双方必须要采用和遵守相同的协议。TCP/IP 四层模型是下面这样的,序列化协议属于哪一层呢?

    • 应用层
    • 传输层
    • 网络层
    • 网络接口层

    在这里插入图片描述
    如上图所示,OSI 七层协议模型中,表示层做的事情主要就是对应用层的用户数据进行处理转换为二进制流。反过来的话,就是将二进制流转换成应用层的用户数据。这不就对应的是序列化和反序列化么?

    因为,OSI 七层协议模型中的应用层、表示层和会话层对应的都是 TCP/IP 四层模型中的应用层,所以序列化协议属于 TCP/IP 协议应用层的一部分。

    4. 常见的序列化协议

    JDK 自带的序列化方式一般不会用 ,因为序列化效率低并且部分版本有安全漏洞。比较常用的序列化协议有 hessian、kyro、protostuff。

    下面提到的都是基于二进制的序列化协议,像 JSON 和 XML 这种属于文本类序列化方式。虽然 JSON 和 XML 可读性比较好,但是性能较差,一般不会选择。

    4.1 JDK 自带的序列化方式

    JDK 自带的序列化,只需实现 java.io.Serializable接口即可。

    @AllArgsConstructor
    @NoArgsConstructor
    @Getter
    @Builder
    @ToString
    public class RpcRequest implements Serializable {
        private static final long serialVersionUID = 1905122041950251207L;
        private String requestId;
        private String interfaceName;
        private String methodName;
        private Object[] parameters;
        private Class<?>[] paramTypes;
        private RpcMessageTypeEnum rpcMessageTypeEnum;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    序列化号 serialVersionUID 属于版本控制的作用。序列化的时候 serialVersionUID 也会被写入二级制序列,当反序列化时会检查 serialVersionUID 是否和当前类的 serialVersionUID 一致。如果 serialVersionUID 不一致则会抛出 InvalidClassException 异常。强烈推荐每个序列化类都手动指定其 serialVersionUID,如果不手动指定,那么编译器会动态生成默认的序列化号

    我们很少或者说几乎不会直接使用这个序列化方式,主要原因有两个:

    1. 不支持跨语言调用 : 如果调用的是其他语言开发的服务的时候就不支持了。
    2. 性能差 :相比于其他序列化框架性能更低,主要原因是序列化之后的字节数组体积较大,导致传输成本加大。

    4.2 Kryo

    Kryo 是一个高性能的序列化/反序列化工具,由于其变长存储特性并使用了字节码生成机制,拥有较高的运行速度和较小的字节码体积。

    另外,Kryo 已经是一种非常成熟的序列化实现了,已经在 Twitter、Groupon、Yahoo 以及多个著名开源项目(如 Hive、Storm)中广泛的使用。

    序列化和反序列化相关的代码如下:

    /**
     * Kryo serialization class, Kryo serialization efficiency is very high, but only compatible with Java language
     *
     * @author shuang.kou
     * @createTime 2020年05月13日 19:29:00
     */
    @Slf4j
    public class KryoSerializer implements Serializer {
    
        /**
         * Because Kryo is not thread safe. So, use ThreadLocal to store Kryo objects
         */
        private final ThreadLocal<Kryo> kryoThreadLocal = ThreadLocal.withInitial(() -> {
            Kryo kryo = new Kryo();
            kryo.register(RpcResponse.class);
            kryo.register(RpcRequest.class);
            return kryo;
        });
    
        @Override
        public byte[] serialize(Object obj) {
            try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
                 Output output = new Output(byteArrayOutputStream)) {
                Kryo kryo = kryoThreadLocal.get();
                // Object->byte:将对象序列化为byte数组
                kryo.writeObject(output, obj);
                kryoThreadLocal.remove();
                return output.toBytes();
            } catch (Exception e) {
                throw new SerializeException("Serialization failed");
            }
        }
    
        @Override
        public <T> T deserialize(byte[] bytes, Class<T> clazz) {
            try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
                 Input input = new Input(byteArrayInputStream)) {
                Kryo kryo = kryoThreadLocal.get();
                // byte->Object:从byte数组中反序列化出对对象
                Object o = kryo.readObject(input, clazz);
                kryoThreadLocal.remove();
                return clazz.cast(o);
            } catch (Exception e) {
                throw new SerializeException("Deserialization failed");
            }
        }
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    异常

    1. Java 异常类层次结构图概览 :

    在这里插入图片描述

    2. Exception 和 Error 有什么区别?

    在 Java 中,所有的异常都有一个共同的祖先 java.lang 包中的 Throwable 类。Throwable 类有两个重要的子类:

    • Exception :程序本身可以处理的异常,可以通过 catch 来进行捕获。Exception 又可以分为 Checked Exception (受检查异常,必须处理) 和 Unchecked Exception (不受检查异常,可以不处理)。
    • Error :Error 属于程序无法处理的错误 ,我们不建议通过catch捕获 。例如 Java 虚拟机运行错误(Virtual MachineError)、虚拟机内存不够错误(OutOfMemoryError)、类定义错误(NoClassDefFoundError)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。

    3. Checked Exception 和 Unchecked Exception 有什么区别?

    Checked Exception 即 受检查异常 ,Java 代码在编译过程中,如果受检查异常没有被 catch或者throws 关键字处理的话,就没办法通过编译。

    比如下面这段 IO 操作的代码:
    在这里插入图片描述
    除了RuntimeException及其子类以外,其他的Exception类及其子类都属于受检查异常 。常见的受检查异常有: IO 相关的异常、ClassNotFoundException 、SQLException…。

    Unchecked Exception 即 不受检查异常 ,Java 代码在编译过程中 ,我们即使不处理不受检查异常也可以正常通过编译。

    RuntimeException 及其子类都统称为非受检查异常,常见的有:

    • NullPointerException(空指针错误)
    • IllegalArgumentException(参数错误比如方法入参类型错误)
    • NumberFormatException(字符串转换为数字格式错误,IllegalArgumentException的子类)
    • ArrayIndexOutOfBoundsException(数组越界错误)
    • ClassCastException(类型转换错误)
    • ArithmeticException(算术错误)
    • SecurityException (安全错误比如权限不够)
    • UnsupportedOperationException(不支持的操作错误比如重复创建同一用户)

    4. Throwable 类常用方法有哪些?

    • String getMessage(): 返回异常发生时的简要描述
    • String toString(): 返回异常发生时的详细信息
    • String getLocalizedMessage(): 返回异常对象的本地化信息。使用 Throwable 的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 getMessage()返回的结果相同
    • void printStackTrace(): 在控制台上打印 Throwable 对象封装的异常信息

    5. try-catch-finally 如何使用?

    • try块 : 用于捕获异常。其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟一个 finally 块。
    • catch块 : 用于处理 try 捕获到的异常。
    • finally 块 : 无论是否捕获或处理异常,finally 块里的语句都会被执行。当在 try 块或 catch 块中遇到 return 语句时,finally 语句块将在方法返回之前被执行。

    代码示例:

    try {
        System.out.println("Try to do something");
        throw new RuntimeException("RuntimeException");
    } catch (Exception e) {
        System.out.println("Catch Exception -> " + e.getMessage());
    } finally {
        System.out.println("Finally");
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    输出:

    Try to do something
    Catch Exception -> RuntimeException
    Finally
    
    
    • 1
    • 2
    • 3
    • 4

    注意:不要在 finally 语句块中使用 return ! 当 try 语句和 finally 语句中都有 return 语句时,try 语句块中的 return 语句会被忽略。这是因为 try 语句中的 return 返回值会先被暂存在一个本地变量中,当执行到 finally 语句中的 return 之后,这个本地变量的值就变为了 finally 语句中的 return 返回值。

    代码示例:

    public static void main(String[] args) {
        System.out.println(f(2));
    }
    
    public static int f(int value) {
        try {
            return value * value;
        } finally {
            if (value == 2) {
                return 0;
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    输出:

    0
    
    
    • 1
    • 2

    6. finally 中的代码一定会执行吗?

    不一定的!在某些情况下,finally 中的代码不会被执行。

    就比如说 finally 之前虚拟机被终止运行的话,finally 中的代码就不会被执行。

    try {
        System.out.println("Try to do something");
        throw new RuntimeException("RuntimeException");
    } catch (Exception e) {
        System.out.println("Catch Exception -> " + e.getMessage());
        // 终止当前正在运行的Java虚拟机
        System.exit(1);
    } finally {
        System.out.println("Finally");
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    输出:

    Try to do something
    Catch Exception -> RuntimeException
    
    
    • 1
    • 2
    • 3

    7. 如何使用 try-with-resources 代替try-catch-finally?

    1. 适用范围(资源的定义): 任何实现 java.lang.AutoCloseable或者 java.io.Closeable 的对象
    2. 关闭资源和 finally 块的执行顺序: 在 try-with-resources 语句中,任何 catch 或 finally 块在声明的资源关闭后运行

    Java 中类似于InputStream、OutputStream 、Scanner 、PrintWriter等的资源都需要我们调用close()方法来手动关闭,一般情况下我们都是通过try-catch-finally语句来实现这个需求,如下:

    //读取文本文件的内容
    Scanner scanner = null;
    try {
        scanner = new Scanner(new File("D://read.txt"));
        while (scanner.hasNext()) {
            System.out.println(scanner.nextLine());
        }
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } finally {
        if (scanner != null) {
            scanner.close();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    使用 Java 7 之后的 try-with-resources 语句改造上面的代码:

    try (Scanner scanner = new Scanner(new File("test.txt"))) {
        while (scanner.hasNext()) {
            System.out.println(scanner.nextLine());
        }
    } catch (FileNotFoundException fnfe) {
        fnfe.printStackTrace();
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    当然多个资源需要关闭的时候,使用 try-with-resources 实现起来也非常简单,如果你还是用try-catch-finally可能会带来很多问题。

    通过使用分号分隔,可以在try-with-resources块中声明多个资源。

    try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));
         BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")))) {
        int b;
        while ((b = bin.read()) != -1) {
            bout.write(b);
        }
    }
    catch (IOException e) {
        e.printStackTrace();
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    泛型

    1. 什么是泛型?有什么作用?

    Java 泛型(Generics) 是 JDK 5 中引入的一个新特性。使用泛型参数,可以增强代码的可读性以及稳定性。

    编译器可以对泛型参数进行检测,并且通过泛型参数可以指定传入的对象类型。比如 ArrayList persons = new ArrayList() 这行代码就指明了该 ArrayList 对象只能传入 Persion 对象,如果传入其他类型的对象就会报错。

    ArrayList<E> extends AbstractList<E>
    
    • 1

    并且,原生 List 返回类型是 Object ,需要手动转换类型才能使用,使用泛型后编译器自动转换。

    2. 泛型的使用方式有哪几种?

    泛型一般有三种使用方式:泛型类泛型接口泛型方法

    1.泛型类:

    //此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
    //在实例化泛型类时,必须指定T的具体类型
    public class Generic<T>{
    
        private T key;
    
        public Generic(T key) {
            this.key = key;
        }
    
        public T getKey(){
            return key;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    如何实例化泛型类:

    Generic<Integer> genericInteger = new Generic<Integer>(123456);
    
    
    • 1
    • 2

    2.泛型接口 :

    public interface Generator<T> {
        public T method();
    }
    
    
    • 1
    • 2
    • 3
    • 4

    实现泛型接口,不指定类型:

    class GeneratorImpl<T> implements Generator<T>{
        @Override
        public T method() {
            return null;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    实现泛型接口,指定类型:

    class GeneratorImpl<T> implements Generator<String>{
        @Override
        public String method() {
            return "hello";
        }
    }`
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    3.泛型方法 :

       public static < E > void printArray( E[] inputArray )
       {
             for ( E element : inputArray ){
                System.out.printf( "%s ", element );
             }
             System.out.println();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    使用:

    // 创建不同类型数组: Integer, Double 和 Character
    Integer[] intArray = { 1, 2, 3 };
    String[] stringArray = { "Hello", "World" };
    printArray( intArray  );
    printArray( stringArray  );
    
    • 1
    • 2
    • 3
    • 4
    • 5
  • 相关阅读:
    基于Flask的岗位就业可视化系统(四)
    学习笔记-GO安全
    GoLang核心知识点
    iai 定向 题解
    2022中国物流产业大会暨企业家高峰论坛在杭州举办!
    利用IPV6随时访问家中影音Jellyfin
    详解TCP为什么不能是两次握手
    HIVE表 DML 操作——第4关:将 select 查询结果写入文件
    springboot+mybatis-plus+element ui生成二维码
    【obs】windows版本号判断
  • 原文地址:https://blog.csdn.net/weixin_45773632/article/details/124996657