Java基础知识之常见面试题整理

声明:本文摘抄自:https://www.cnblogs.com/qing-gee/p/13258782.html

1.请说出Java14版本中更新的重要功能?

  1)switch表达式

    关于 switch 表达式,这里就简单地举个例子

public class SwitchDemo {
    enum PlayerTypes {
        TENNIS,
        FOOTBALL,
        BASKETBALL,
        PINGPANG,
        UNKNOWN
    }

    public static void main(String[] args) {
        System.out.println(createPlayer(PlayerTypes.BASKETBALL));
    }

    private static String createPlayer(PlayerTypes playerType) {
        return switch (playerType) {
            case TENNIS -> "网球运动员费德勒";
            case FOOTBALL -> "足球运动员C罗";
            case BASKETBALL -> "篮球运动员詹姆斯";
            case PINGPANG -> "乒乓球运动员马龙";
            case UNKNOWN -> throw new IllegalArgumentException("未知");
        };
    }
}

   除了可以使用 -> 的新式语法,还可以作为 return 结果.

  2)instanceof增强表达式,预览功能

    旧式的 instanceof 的用法如下所示:

public class OldInstanceOf {
    public static void main(String[] args) {
        Object str = "Java 14,真香";
        if (str instanceof String) {
            String s = (String)str;
            System.out.println(s.length());
        }
    }
}

    需要先使用 instanceof 在 if 条件中判断 str 的类型是否为 String(第一步),再在  if 语句中将 str 强转为字符串类型(第二步),并且要重新声明一个变量用于强转后的赋值(第三步)。

    Java14中的新用法:

public class NewInstanceOf {
    public static void main(String[] args) {
        Object str = "Java 14,真香";
        if (str instanceof String s) {
            System.out.println(s.length());
        }
    }
}

    可以直接在 if 条件判断类型的时候添加一个变量,就不需要再强转和声明新的变量了。

  3)文本块,第二次预览

    在文本块(Text Blocks)出现之前,如果我们需要拼接多行的字符串,就需要很多英文双引号和加号,非常不雅。如果恰好要拼接一些HTML 格式的文本(原生 SQL 也是如此)的话,还要通过空格进行排版,通过换行转义符 \n 进行换行,这些繁琐的工作对于一名开发人员来说,简直就是灾难。

public class OldTextBlock {
    public static void main(String[] args) {
        String html = "<html>\n" +
                "    <body>\n" +
                "        <p>Hello, world</p>\n" +
                "    </body>\n" +
                "</html>\n";
        System.out.println(html);
    }
}

    Java 14 就完全不同了:

public class NewTextBlock {
    public static void main(String[] args) {
        String html = """
              <html>
                  <body>
                      <p>Hello, world</p>
                  </body>
              </html>
              """;
        System.out.println(html);
    }
}

   多余的英文双引号、加号、换行转义符,统统不见了。仅仅是通过前后三个英文双引号就实现了。

  4)Records,预览功能

    对于 Records 来说,一条 Record 就代表一个不变的状态。

2.请说出Java13版本中更新的重要功能?

  1)文本块,预览功能.

  2)switch表达式,预览功能.

  3)Java Socket 重新实现

  4)FileSystems.newFileSystem()方法

  5)支持Unicode 12.1

  6)可伸缩,低延迟的垃圾收集器改进,用于返回未使用的内存.

3.请说出Java12版本中重要的更新功能?

  1)JVM的更新

  2)File.mismatch()方法

  3)紧凑型数字格式

  4)String类新增了一些方法,比如indent()

4.请说出Java11版本中更新的重要功能?

  1)可以直接使用Java命令运行Java程序,源代码将会隐式编译和运行.

  2)String类新增了一些方法,比如说isBlank(),lines(),strip()等待.

  3)Files类新增了两个读写方法,readString()和writeString().

  4)可以在Lambda表达式中使用var作为变量类型.

5.请说出Java10版本中更新的重要功能?

  1)局部变量类型推断,举个例子:var list = new ArrayList();可以使用var来作为变量类型,Java编译器知道list的类型为字符串的ArrayList;

  2)增强java.util.Locale.

  3)提供了一种默认的根证书颁发机构(CA)

6.请说出Java9版本中更新的重要功能?

  1)模块系统

  2)不可变的List,Set,Map的工厂方法

  3)接口中可以有私有方法

  4)垃圾收集器的改进

7.请说出Java8版本中更新的重要功能?

  1)函数式编程和Lambda表达式

    Lambda 表达式描述了一个代码块(或者叫匿名方法),可以将其作为参数传递给构造方法或者普通方法以便后续执行。

() -> System.out.println("沉默王二")

    从左到右解释一下,() 为 Lambda 表达式的参数列表(本例中没有参数),->标识这串代码为 Lambda 表达式(也就是说,看到 -> 就知道这是 Lambda),System.out.println("沉默王二") 为要执行的代码,即将“沉默王二”打印到标准输出流。

    使用Lambda表达式创建一个线程:

    原来我们创建一个线程并启动它是这样的:

public class LamadaTest {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("沉默王二");
            }
        }).start();
    }
}

    通过 Lambda 表达式只需要下面这样:

public class LamadaTest {
    public static void main(String[] args) {
        new Thread(() -> System.out.println("沉默王二")).start();
    }
}

  2)Stream流

    Stream 就好像一个高级的迭代器,但只能遍历一次,就好像一江春水向东流;在流的过程中,对流中的元素执行一些操作,比如“过滤掉长度大于 10 的字符串”、“获取每个字符串的首字母”等。

    要想操作流,首先需要有一个数据源,可以是数组或者集合。每次操作都会返回一个新的流对象,方便进行链式操作,但原有的流对象会保持不变。

    流的操作可以分为两种类型:

      1)中间操作,可以有多个,每次返回一个新的流,可进行链式操作。

      2)终端操作,只能有一个,每次执行完,这个流也就用光光了,无法执行下一个操作,因此只能放在最后。

      来举个例子。

      List<String> list = new ArrayList<>();
        list.add("武汉加油");
        list.add("中国加油");
        list.add("世界加油");
        list.add("世界加油");

      long count = list.stream().distinct().count();

      System.out.println(count);

      distinct() 方法是一个中间操作(去重),它会返回一个新的流(没有共同元素)。

      Stream<T> distinct();

      count() 方法是一个终端操作,返回流中的元素个数。

      long count();

      中间操作不会立即执行,只有等到终端操作的时候,流才开始真正地遍历,用于映射、过滤等。通俗点说,就是一次遍历执行多个操作,性能就大大提高了。

  3)Java Date Time API

  4)接口中可以使用默认方法和静态方法

  接口是通过 interface 关键字定义的,它可以包含一些常量和方法,来看下面这个示例。

public interface Electronic {
    // 常量
    String LED = "LED";

    // 抽象方法
    int getElectricityUse();

    // 静态方法
    static boolean isEnergyEfficient(String electtronicType) {
        return electtronicType.equals(LED);
    }

    // 默认方法
    default void printDescription() {
        System.out.println("电子");
    }
}

  1)接口中定义的变量会在编译的时候自动加上 public static final 修饰符,也就是说 LED 变量其实是一个常量。

  2)getElectricityUse()其实是一个抽象方法,没有方法体——这是定义接口的本意。

  3)从 Java 8 开始,接口中允许有静态方法,比如说 isEnergyEfficient() 方法。

    静态方法无法由(实现了该接口的)类的对象调用,它只能通过接口的名字来调用,比如说 Electronic.isEnergyEfficient("LED")

  4)接口中允许定义 default 方法也是从 Java 8 开始的,比如说 printDescription(),它始终由一个代码块组成,为实现该接口而不覆盖该方法的类提供默认实现,也就是说,无法直接使用一个“;”号来结束默认方法——编译器会报错的。

8.Java面向对象编程中的一些重要概念?

  1)抽象

    抽象类的介绍:

      1)定义抽象类的时候需要用到关键字 abstract,放在 class 关键字前。

public abstract class AbstractPlayer {
}

      2)抽象类不能被实例化,但可以有子类。通过 extends 关键字可以继承抽象类.

public class BasketballPlayer extends AbstractPlayer {
}

      3)如果一个类定义了一个或多个抽象方法,那么这个类必须是抽象类

     4)抽象类可以同时声明抽象方法和具体方法,也可以什么方法都没有.

public abstract class AbstractPlayer {
    abstract void play();

    public void sleep() {
        System.out.println("运动员也要休息而不是挑战极限");
    }
}

      5)抽象类派生的子类必须实现父类中定义的抽象方法。

public class BasketballPlayer extends AbstractPlayer {
    @Override
    void play() {
        System.out.println("我是张伯伦,篮球场上得过 100 分");
    }
}

    什么时候使用抽象类?抽象类的使用场景?

      1)我们希望一些通用的功能被多个子类复用。比如说,AbstractPlayer 抽象类中有一个普通的方法 sleep(),表明所有运动员都需要休息,那么这个方法就可以被子类复用。

public abstract class AbstractPlayer {
    public void sleep() {
        System.out.println("运动员也要休息而不是挑战极限");
    }
}

      BasketballPlayer 继承了 AbstractPlayer 类,也就拥有了 sleep() 方法。

public class BasketballPlayer extends AbstractPlayer {
}

      BasketballPlayer 对象可以直接调用 sleep() 方法:

BasketballPlayer basketballPlayer = new BasketballPlayer();
basketballPlayer.sleep();

      FootballPlayer 继承了 AbstractPlayer 类,也就拥有了 sleep() 方法。

public class FootballPlayer extends AbstractPlayer {
}

      FootballPlayer 对象也可以直接调用 sleep() 方法:

FootballPlayer footballPlayer = new FootballPlayer();
footballPlayer.sleep();

      2)我们需要在抽象类中定义好 API,然后在子类中扩展实现。比如说,AbstractPlayer  抽象类中有一个抽象方法 play(),定义所有运动员都可以从事某项运动,但需要对应子类去扩展实现。

public abstract class AbstractPlayer {
    abstract void play();
}

      BasketballPlayer 继承了 AbstractPlayer 类,扩展实现了自己的 play() 方法。

public class BasketballPlayer extends AbstractPlayer {
    @Override
    void play() {
        System.out.println("我是张伯伦,我篮球场上得过 100 分,");
    }
}

      FootballPlayer 继承了 AbstractPlayer 类,扩展实现了自己的 play() 方法。

public class FootballPlayer extends AbstractPlayer {
    @Override
    void play() {
        System.out.println("我是C罗,我能接住任意高度的头球");
    }
}

  2)封装

  3)多态

  4)继承

9.什么是JVM?

  JVM(Java Virtual Machine)俗称Java虚拟机,之所以称为虚拟机,是因为它实际上并不存在,它提供了一种运行环境,可供Java字节码在上面运行.

  JVM提供了以下操作:

    1)加载字节码

    2)验证字节码

    3)执行字节码

    4)提供运行时环境

  JVM定义了以下内容:

    1)存储区

    2)类文件格式

    3)寄存器组

    4)垃圾回收堆

    5)致命错误报告等.

  JVM的内部结构,它包含了类加载器(ClassLoader),运行时数据区(Runtime Data Areas)和执行引擎(Excution Engine).

  

   1)类加载器:

    类加载器是 JVM 的一个子系统,用于加载类文件。每当我们运行一个 Java 程序,它都会由类加载器首先加载。Java 中有三个内置的类加载器:

      启动类加载器(Bootstrap Class-Loader),加载 jre/lib 包下面的 jar 文件,比如说常见的rt.jar(包含了 Java 标准库下的所有类文件,比如说 java.lang 包下的类,java.net 包下的类,java.util 包下的类,java.io 包下的类,java.sql 包下的类)。

      扩展类加载器(Extension or Ext Class-Loader),加载 jre/lib/ext 包下面的 jar 文件。

      应用类加载器(Application or App Clas-Loader),根据程序的类路径(classpath)来加载 Java类。

    一般来说,Java 程序员并不需要直接同类加载器进行交互。JVM 默认的行为就已经足够满足大多数情况的需求了。不过,如果遇到了需要和类加载器进行交互的情况,而对类加载器的机制又不是很了解的话,就不得不花大量的时间去调试ClassNotFoundException 和 NoClassDefFoundError 等异常。

    对于任意一个类,都需要由它的类加载器和这个类本身一同确定其在 JVM 中的唯一性。也就是说,如果两个类的加载器不同,即使两个类来源于同一个字节码文件,那这两个类就必定不相等(比如两个类的 Class对象不 equals)。

    通过一段简单的代码了解下。

public class Test {

    public static void main(String[] args) {
        ClassLoader loader = Test.class.getClassLoader();
        while (loader != null) {
            System.out.println(loader.toString());
            loader = loader.getParent();
        }
    }
}

    每个 Java 类都维护着一个指向定义它的类加载器的引用,通过 类名.class.getClassLoader() 可以获取到此引用;然后通过 loader.getParent() 可以获取类加载器的上层类加载器。

    上面这段代码的输出结果如下:

sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@4617c264

      第一行输出为 Test 的类加载器,即应用类加载器,它是 sun.misc.Launcher$AppClassLoader 类的实例;

    第二行输出为扩展类加载器,是 sun.misc.Launcher$ExtClassLoader 类的实例。那启动类加载器呢?

    按理说,扩展类加载器的上层类加载器是启动类加载器,但在我这个版本的 JDK 中, 扩展类加载器的 getParent() 返回 null。所以没有输出。

  2)运行时数据区

    运行时数据区又包含以下内容。

    

     PC寄存器(PC Register),也叫程序计数器(Program Counter Register),是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的信号指示器,线程私有

    JVM 栈(Java Virtual Machine Stack),与 PC 寄存器一样,JVM 栈也是线程私有的。每一个JVM 线程都有自己的 JVM 栈,这个栈与线程同时创建,它的生命周期与线程相同。它的作用是为虚拟机执行java字节码,或者说来解析java语法是否正确.

    本地方法栈(Native Method Stack),JVM 可能会使用到传统的栈来支持 Native 方法(使用Java 语言以外的其它语言[C语言]编写的方法)的执行,这个栈就是本地方法栈。它的作用和JVM差不过,只不过JVM解析的是java字节码,本地方法栈解析其他语言的字节码,比如需要调用c语言的函数,就使用它来解析的.

    堆(Heap)在 JVM 中,堆是可供各条线程共享的运行时内存区域,也是供所有类实例和数据对象分配内存的区域。

    方法区(Method area),在 JVM 中,被加载类型的信息都保存在方法区中。包括类型信息(TypeInformation)和方法列表(Method Tables)。方法区是所有线程共享的,所以访问方法区信息的方法必须是线程安全的。

    运行时常量池(Runtime Constant Pool),运行时常量池是每一个类或接口的常量池在运行时的表现形式,它包括了编译器可知的数值字面量,以及运行期解析后才能获得的方法或字段的引用。简而言之,当一个方法或者变量被引用时,JVM 通过运行时常量区来查找方法或者变量在内存里的实际地址

  3)执行引擎

    执行引擎包含了:

      解释器:读取字节码流,然后执行指令。因为它一条一条地解释和执行指令,所以它可以很快地解释字节码,但是执行起来会比较慢。

      即时(Just-In-Time,JIT)编译器:即时编译器用来弥补解释器的缺点,提高性能。执行引擎首先按照解释执行的方式来执行,然后在合适的时候,即时编译器把整段字节码编译成本地代码。然后,执行引擎就没有必要再去解释执行方法了,它可以直接通过本地代码去执行。执行本地代码比一条一条进行解释执行的速度快很多。编译后的代码可以执行的很快,因为本地代码是保存在缓存里的。

10.JDK和JVM有什么区别?

  JDK 是 Java Development Kit 的首字母缩写,是提供给 Java 开发人员的软件环境,包含 JRE 和一组开发工具。可分为以下版本:

    标准版(大多数开发人员用的就是这个)

    企业版

    微型版

  JDK 包含了一个私有的 JVM 和一些其他资源,比如说编译器(javac 命令)、解释器(java 命令)等,帮助 Java 程序员完成开发工作。

11.JVM和JRE有什么区别?

  Java Runtime Environment(JRE)是 JVM 的实现。JRE 由 JVM 和 Java 二进制文件以及其他类组成,可以执行任何程序。JRE 不包含 Java 编译器,调试器等任何开发工具。

  

 12.Java中main()方法的重要性是什么?

  每个程序都需要一个入口,对于 Java 程序来说,入口就是 main 方法

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

  public 关键字是另外一个访问修饰符,除了可以声明方法和变量(所有类可见),还可以声明类。main() 方法必须声明为 public。

  static 关键字表示该变量或方法是静态变量或静态方法,可以直接通过类访问,不需要实例化对象来访问。

  void 关键字用于指定方法没有返回值。

  另外,main 关键字为方法的名字,Java 虚拟机在执行程序时会寻找这个标识符;args 为 main() 方法的参数名,它的类型为一个 String 数组,也就是说,在使用 java 命令执行程序的时候,可以给 main() 方法传递字符串数组作为参数。

java HelloWorld 沉默王二 沉默王三//启动类的main方法时传递了两个字符串参数

   javac 命令用来编译程序,java 命令用来执行程序,HelloWorld 为这段程序的类名,沉默王二和沉默王三为字符串数组,中间通过空格隔开,然后就可以在 main() 方法中通过 args[0] 和 args[1] 获取传递的参数值了。

public class HelloWorld {
    public static void main(String[] args) {
        if ("沉默王二".equals(args[0])) {

        }

        if ("沉默王三".equals(args[1])) {

        }
    }
}

13.Java的重载(Overload)和重写(Override)有什么区别?

class LaoWang{
    public void write() {
        System.out.println("老王写了一本《基督山伯爵》");
    }
}
public class XiaoWang extends LaoWang {
    @Override
    public void write() {
        System.out.println("小王写了一本《茶花女》");
    }
}

  重写的两个方法名相同方法参数的个数也相同;不过一个方法在父类中,另外一个在子类中。就好像父类LaoWang 有一个 write() 方法(无参),方法体是写一本《基督山伯爵》;子类 XiaoWang 重写了父类的 write() 方法(无参),但方法体是写一本《茶花女》。

  测试代码:

public class OverridingTest {
    public static void main(String[] args) {
        LaoWang wang = new XiaoWang();
        wang.write();
    }
}

  测试结果:

小王写了一本《茶花女》

  在上面的代码中,们声明了一个类型为 LaoWang 的变量 wang。在编译期间,编译器会检查 LaoWang类是否包含了 write() 方法,发现 LaoWang 类有,于是编译通过。在运行期间,new 了一个XiaoWang 对象,并将其赋值给 wang,此时 Java 虚拟机知道 wang 引用的是 XiaoWang 对象,所以调用的是子类 XiaoWang 中的 write() 方法而不是父类 LaoWang 中的 write() 方法,因此输出结果为“小王写了一本《茶花女》”。

  再来看一段重载的代码吧。

class LaoWang{
    public void read() {
        System.out.println("老王读了一本《Web全栈开发进阶之路》");
    }

    public void read(String bookname) {
        System.out.println("老王读了一本《" + bookname + "》");
    }
}

   重载的两个方法名相同,但方法参数的个数不同,另外也不涉及到继承两个方法在同一个类中。就好像类LaoWang 有两个方法,名字都是 read(),但一个有参数(书名),另外一个没有(只能读写死的一本书)。

  测试代码:

public class OverloadingTest {
    public static void main(String[] args) {
        LaoWang wang = new LaoWang();
        wang.read();
        wang.read("金瓶");
    }
}

  这结果就不用猜了。变量 wang 的类型为 LaoWang,wang.read() 调用的是无参的 read() 方法,因此先输出“老王读了一本《Web全栈开发进阶之路》”;wang.read("金瓶") 调用的是有参的 read(bookname) 方法,因此后输出“老王读了一本《金瓶》”。在编译期间,编译器就知道这两个 read() 方法是不同的,因为它们的方法签名(=方法名称+方法参数)不同。

  简单来总结一下:

    1)编译器无法决定调用哪个重写的方法,因为只从变量的类型上是无法做出判断的,要在运行时才能决定;但编译器可以明确地知道该调用哪个重载的方法,因为引用类型是确定的,参数个数决定了该调用哪个方法。

    2)多态针对的是重写,而不是重载。

    如果在一个类中有多个相同名字的方法,但参数不同,则称为方法重载。

    父类中有一个方法,子类中有另外一个和它有相同签名(方法名相同,参数相同、修饰符相同)的方法时,则称为方法重写。子类在重写父类方法的时候可以加一个 @Override 注解。

14.说一下java的访问权限修饰符?

  访问权限修饰符对于 Java 来说,非常重要,目前共有四种:publicprivateprotecteddefault(缺省)。

  一个类只能使用 public 或者 default 修饰,缺省意味着这个类可以被同一个包下的其他类进行访问;而 public 意味着这个类可以被所有包下的类进行访问。

  private 可以用来修饰类的构造方法、字段和方法,只能被当前类进行访问。

  protected 也可以用来修饰类的构造方法、字段和方法,但它的权限范围更宽一些,可以被同一个包中的类进行访问,或者当前类的子类。

  可以通过下面这张图来对比一下四个权限修饰符之间的差别:

  

15.final关键字的理解?

  1)final 关键字修饰类的时候,表示该类无法被继承。比如,String 类就是 final 的,无法被继承。

  2)final 关键字修饰方法的时候,表示子类无法覆盖它,也就是不能重写此方法。

  3)final 关键字修饰变量的时候,表示该变量只能被赋值一次,尽管变量的状态可以更改。

    3.1)final修饰的字段可以分为两种:一种是static类型的,另一种是没有static类型的

      非 static 的 final 字段必须有一个默认值,否则编译器将会提醒没有初始化

      static 的 final 字段也叫常量,它的名字应该为大写,可以在声明的时候初始化,也可以通过 static [代码块初始化]()。

    3.2)final修饰的参数,final 关键字还可以修饰参数,它意味着参数在方法体内不能被再修改:如果尝试去修改它的话,编译器会提示以下错误:

      

16.static关键字的理解?

static 关键字可用于变量、方法、代码块和内部类,表示某个特定的成员只属于某个类本身,而不是该类的某个对象。

  1)static 关键字可以用来修饰类变量使其具有全局性,即所有对象将共享同一个变量。它属于一个类,而不是这个类的对象。

  2)static 关键字可以用来修饰方法,该方法称为静态方法,只可以访问类的静态变量,并且只能调用类的静态方法。

    1.Java 中的静态方法在编译时解析,因为静态方法不能被重写(方法重写发生在运行时阶段,为了多态)。

    2.抽象方法不能是静态的。

    3.静态方法不能使用 this 和 super 关键字。

    4.成员方法可以直接访问其他成员方法和成员变量。

    5.成员方法也可以直接访问静态方法和静态变量。

    6.静态方法可以访问所有其他静态方法和静态变量。

    7.静态方法无法直接访问成员方法和成员变量。

      

  3)静态代码块可以用来初始化静态变量,尽管静态方法也可以在声明的时候直接初始化,但有些时候,我们需要多行代码来完成初始化。

public class StaticBlockDemo {
    public static List<String> writes = new ArrayList<>();

    static {
        writes.add("沉默王二");
        writes.add("沉默王三");
        writes.add("沉默王四");

        System.out.println("第一块");
    }

    static {
        writes.add("沉默王五");
        writes.add("沉默王六");

        System.out.println("第二块");
    }
}

    writes 是一个静态的 ArrayList,所以不太可能在声明的时候完成初始化,因此需要在静态代码块中完成初始化。

    1.一个类可以有多个静态代码块。

    2.静态代码块的解析和执行顺序和它在类中的位置保持一致。

  4)静态内部类

    Java 允许我们在一个类中声明一个内部类,它提供了一种令人信服的方式,允许我们只在一个地方使用一些变量,使代码更具有条理性和可读性。

    常见的内部类有四种,成员内部类、局部内部类、匿名内部类和静态内部类.

public class Singleton {
    private Singleton() {}

    private static class SingletonHolder {
        public static final Singleton instance = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }
}

    这就是创建单例的一种方式,第一次加载Singleton 类时并不会初始化 instance,只有第一次调用 getInstance() 方法时Java 虚拟机才开始加载 SingletonHolder 并初始化 instance,这样不仅能确保线程安全也能保证 Singleton 类的唯一性。

    1.静态内部类不能访问外部类的所有成员变量。

    2.静态内部类可以访问外部类的所有静态变量,包括私有变量。

    3.外部类不能声明为 static。

17.finally和finalize的区别?

  1)finally 通常与 try-catch 块一起使用,即使 try-catch 块引发了异常,finally 块中的代码也会被执行,用于释放 try 块中创建的资源。

  2)finalize() 是 Object 类的一个特殊方法,当对象正在被垃圾回收时,垃圾收集器将会调用该方法。可以重写该方法用于释放系统资源

18.可以将一个类声明为static的吗?

  不能将一个外部类声明为 static 的,但可以将一个内部类声明为 static 的——称为静态内部类。

19.什么是try-with-resources?

  try-with-resources 是 Java 7 时引入的一个自动资源管理语句,在此之前,我们必须通过 try-catch-finally 的方式手动关闭资源,当我们忘记关闭资源的时候,就容易导致内存泄漏。

  以前我们完成读取文件的功能时,经常会使用try-catch-finally,在finally语句中关闭资源:

public class TrycatchfinallyDecoder {
    public static void main(String[] args) {
        BufferedReader br = null;
        try {
            String path = TrycatchfinallyDecoder.class.getResource("/牛逼.txt").getFile();//确定文件在resource文件下
            String decodePath = URLDecoder.decode(path,"utf-8");//解决读取中文名的文件名不乱码问题
            br = new BufferedReader(new FileReader(decodePath));

            String str = null;
            while ((str =br.readLine()) != null) {
                System.out.println(str);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {//关闭资源
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

  try–catch-finally 至始至终存在一个严重的隐患:try 中的 br.readLine()有可能会抛出 IOException,finally 中的 br.close() 也有可能会抛出 IOException。假如两处都不幸地抛出了 IOException,那程序的调试任务就变得复杂了起来,而且还有可能br.close()的异常覆盖br.readLine()的异常,到底是哪一处出了错误,就需要花一番功夫,这是我们不愿意看到的结果。

  但自从有了 try-with-resources,这些问题就迎刃而解了,只要需要释放的资源(比如 BufferedReader)实现了 AutoCloseable 接口。有了解决方案之后,我们来对之前的 finally 代码块进行瘦身。

try (BufferedReader br = new BufferedReader(new FileReader(decodePath));) {//把要释放的资源写在 try 后的 () 中
    String str = null;
    while ((str =br.readLine()) != null) {
        System.out.println(str);
    }
} catch (IOException e) {
    e.printStackTrace();
}//finally 代码块消失了

  如果有多个资源(BufferedReader 和 PrintWriter)需要释放的话,可以直接在() 中添加。

try (BufferedReader br = new BufferedReader(new FileReader(decodePath));
     PrintWriter writer = new PrintWriter(new File(writePath))) {//有多个资源需要释放的时候
    String str = null;
    while ((str =br.readLine()) != null) {
        writer.print(str);
    }
} catch (IOException e) {
    e.printStackTrace();
}

  如果你想释放自定义资源的话,只要让它实现 AutoCloseable 接口,并提供 close() 方法即可。

class MyResource implements AutoCloseable {
    @Override
    public void close() throws Exception {
        System.out.println("关闭自定义资源");
    }
}
public class TrywithresourcesCustom {
    public static void main(String[] args) {
        try (MyResource resource = new MyResource();) {
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

  代码运行后输出的结果如下所示:

关闭自定义资源

  我们在 try () 中只是 new 了一个 MyResource 的对象,其他什么也没干,但偏偏 close() 方法中的输出语句执行了。想要知道为什么吗?来看看反编译后的字节码吧。

class MyResource implements AutoCloseable {
    MyResource() {
    }

    public void close() throws Exception {
        System.out.println("关闭自定义资源");
    }
}
public class TrywithresourcesCustom {
    public TrywithresourcesCustom() {
    }

    public static void main(String[] args) {
        try {
            MyResource resource = new MyResource();
            resource.close();//自己主动调用了close方法
        } catch (Exception var2) {
            var2.printStackTrace();
        }

    }
}

20.什么是multi-catch?

  Java 7 改进的另外一个地方就是 multi-catch,可以在单个 catch 中捕获多个异常,当一个 try 块抛出多多个类似的异常时,这种写法更短,更清晰。

catch(IOException | SQLException ex){
     logger.error(ex);
     throw new MyException(ex.getMessage());
}

  当有多个异常的时候,可以使用管道表示符“|”隔开。

21.什么是static块?

  static 块是由 Java ClassLoader 将类加载到内存中时执行的代码块。通常用于初始化类的静态变量或者创建静态资源。

22.说一下你对java中接口的理解?

  接口是 Java 编程语言中的一个核心概念,不仅在 JDK 源码中使用很多,还在 Java 设计模式、框架和工具中使用很多。接口提供了一种在 Java 中实现抽象的方法,用于定义子类的行为约定。

  接口和抽象类的区别?

    1)语法层面上:

      接口中不能有 public 和 protected 修饰的方法,抽象类中可以有。

      接口中的变量只能是隐式的常量,抽象类中可以有任意类型的变量。

      一个类只能继承一个抽象类,但却可以实现多个接口。

    2)设计层面上:

      抽象类是对类的一种抽象,继承抽象类的类和抽象类本身是一种 is-a 的关系。

      接口是对类的某种行为的一种抽象,接口和类之间并没有很强的关联关系,所有的类都可以实现 Serializable 接口,从而具有序列化的功能。

23.说一下String类的理解,以及对不可变类(t_claim_green_flag类)的了解?

  什么是不可变类:

    一个类的对象在通过构造方法创建后如果状态不会再被改变,那么它就是一个不可变(immutable)类。它的所有成员变量的赋值仅在构造方法中完成,不会提供任何setter 方法供外部类去修改。

  String类为什么要设计成不可变的?

    1)常量池的需要

      字符串常量池是 Java 堆内存中一个特殊的存储区域,当创建一个 String 对象时,假如此字符串在常量池中不存在,那么就创建一个;假如已经存,就不会再创建了,而是直接引用已经存在的对象。这样做能够减少 JVM 的内存开销,提高效率。

    2)hashCode的需要

      因为字符串是不可变的,所以在它创建的时候,其 hashCode 就被缓存了,因此非常适合作为哈希值(比如说作为 HashMap 的键),多次调用只返回同一个值,来提高效率。

    3)线程安全

      就像之前说的那样,如果对象的状态是可变的,那么在多线程环境下,就很容易造成不可预期的结果。而 String 是不可变的,就可以在多个线程之间共享,不需要同步处理。

      因此,当我们调用 String 类的任何方法(比如说 trim()substring()toLowerCase())时,总会返回一个新的对象,而不影响之前的值。

    除了 String 类,包装器类 Integer、Long 等也是不可变类。

  自定义一个不可变类:

    一个不可变类,必须要满足以下4 个条件:

      1)确保类是 final 的,不允许被其他类继承。

      2)确保所有的成员变量(字段)是 final 的,这样的话,它们就只能在构造方法中初始化值,并且不会在随后被修改。

      3)不要提供任何 setter 方法。

      4)如果要修改类的状态,必须返回一个新的对象。

    按照以上条件,我们来自定义一个简单的不可变类 Writer。

public final class Writer {
    private final String name;
    private final int age;

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

    public int getAge() {
        return age;
    }

    public String getName() {
        return name;
    }
}

    Writer 类是 final 的,name 和 age 也是 final 的,没有 setter 方法。

    定义一个可变类:Book 类

public class Book {
    private String name;
    private int price;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }

    @Override
    public String toString() {
        return "Book{" +
                "name='" + name + '\'' +
                ", price=" + price +
                '}';
    }
}

    2 个字段,分别是 name 和 price,以及 getter 和 setter,重写后的 toString()方法。然后,在 Writer 类中追加一个可变对象字段 book。

public final class Writer {
    private final String name;
    private final int age;
    private final Book book;

    public Writer(String name, int age, Book book) {
        this.name = name;
        this.age = age;
        this.book = book;
    }

    public int getAge() {
        return age;
    }

    public String getName() {
        return name;
    }

    public Book getBook() {
        return book;
    }
}

    并在构造方法中追加了 Book 参数,以及 Book 的 getter 方法。

     完成以上工作后,我们来新建一个测试类,看看 Writer 类的状态是否真的不可变。

public class WriterDemo {
    public static void main(String[] args) {
        Book book = new Book();
        book.setName("Web全栈开发进阶之路");
        book.setPrice(79);

        Writer writer = new Writer("沉默王二",18, book);
        System.out.println("定价:" + writer.getBook());
        writer.getBook().setPrice(59);
        System.out.println("促销价:" + writer.getBook());
    }
}

    程序输出的结果如下所示:

定价:Book{name='Web全栈开发进阶之路', price=79}
促销价:Book{name='Web全栈开发进阶之路', price=59}

    糟糕,Writer 类的不可变性被破坏了,价格发生了变化。为了解决这个问题,我们需要为不可变类的定义规则追加一条内容:

    如果一个不可变类中包含了可变类的对象,那么就需要确保返回的是可变对象的副本。也就是说,Writer 类中的 getBook() 方法应该修改为:

public Book getBook() {
    Book clone = new Book();
    clone.setPrice(this.book.getPrice());
    clone.setName(this.book.getName());
    return clone;
}

    这样的话,构造方法初始化后的 Book 对象就不会再被修改了。此时,运行WriterDemo,就会发现价格不再发生变化了。

定价:Book{name='Web全栈开发进阶之路', price=79}
促销价:Book{name='Web全栈开发进阶之路', price=79}

 

posted @ 2020-07-07 11:39  WK_BlogYard  阅读(361)  评论(0编辑  收藏  举报