JAVA面试汇总第一章 基础强化

564 篇文章 136 订阅

如何轻松获得 Offer

你好,我是王磊,某上市公司技术研发经理,前奇虎 360 员工,有着 10
余年的编程工作经验,目前主要负责新员工技术面试和构建企业技术架构的相关事宜。随着面试过的人数增加,我发现面试者们暴露出了技术方面的很多问题,为了让更多面试者少走一些弯路,也为了让企业能招到合适的技术人才,于是就诞生了这个专栏。

为了写好这个专栏内容,我先后拜访了一二十家互联网公司,与不同的面试官和面试者进行面对面探讨,深入了解了企业对于面试者的要求和常见的 Java
面试题型。之后我花了大半年的时间,结合自己 4 年多作为面试官的经历,把这些内容整理成文,用大约 10 万字的内容对 Java 的核心知识点和常见的 500
多道面试题,做了详细的介绍,也就是本专栏中你所看到的全部内容,希望对你能有所帮助。

为什么要学这个专栏内容?

「因为它能为你赢得面试的主动权,让你获得更多的 Offer。」

从业十多年,我从面试者变成面试官,在 Java 面试上积累了比较丰富的经验。

其实,很多面试者在搜集面试资料的时候都踩过一些“坑”,你是不是也遇到过:

  • 免费搜索的面试题,内容 不全面 ,这就算了,有时候答案都 不准确
  • 很多培训机构提供的面试宝典内容虽然不少,但 深度不够 ,且面试题 过于老旧 脱离了企业实际需要;
  • 还有很多付费的面试题存在滥竽充数,提供了很多没有价值的面试题, 钱花了,干货没学到
  • 市面上大部分面试题只讲了基础概念, 没有提供题目解析和示例代码 ,不利于读者真正的掌握背后的原理,只能死记硬背,且容易忘记。

为了规避这些“坑”,我跑了很多家互联网公司,来确认 Java
面试中实际考察的高频知识点和常见题型。可是有了第一手素材后,我要如何让大家真正从我的讲解中学到干货、用到实处呢?

经过反复验证,我才设计了如下的内容讲述模式。

第一,500+ 面试题详解。

如果你是还没走入职场的新人,我会为你提供完整的 Java 技术栈讲解,以及 最新、最全、最实用 的 500 多道 Java 面试题详解。

第二,10万字 Java 核心知识点梳理。

本专栏的每一篇内容,都采用的是「核心知识点 + N 道相关面试题」的模式,让你不单能应付面试,还能学到更多的 Java 核心知识。

第三,技术、面试搭配平衡,不但让你学到心里,还助你展示出来。

面对目前技术市场的相对冷淡和一个职位多个应聘者竞争的现状,面试者们只有掌握更多 Java 核心技能和面试理论知识,才能在众多面试者中脱颖而出。

本专栏每篇文章大致分为两个部分: Java 核心点介绍 + 相关面试题详解
,这两部分内容相辅相成,前面的核心知识点介绍让后面的面试题更容易理解,后面的面试题加深了读者对于 Java
核心点的掌握。如此一来,让你所学及所用,不仅能够应付面试,更能学习到更多有价值的 Java 技术点,让你在面试中和工作中都能展示的更加出色。

专栏大纲

本专栏分为七大部分,共计 37 讲,约 10 万字。

第一部分:Java 基础强化

这部分包含 7 篇文章,我会从 Java 最基础的内容讲起。有最常见的 String 面试题从表象到原理的深入讲解;还有 Java 8
中新特性的介绍,比如时间和日期模块,让你使用更简洁和优化的方式写出更完美的代码;还有我们日常用的很多包装类不为人知的有趣现象和知识盲点介绍;还有数组以及算法的介绍,虽然基础但容易被面试者忽略和容易出错的问题……

第二部分:各种类和克隆

这部分包含 4 篇文章,除了会深入讲解 Java
中的各种类和接口的相关内容,还会深入讲解浅克隆和深克隆的各种实现方式,以及配合各种图片让你更形象地理解深/浅克隆的本质。

第三部分:数据结构和队列

这部分包含 4
篇文章,对面试中必考的集合,除了相关的面试题讲解,更要理清各种集合之间的关系,创建集合之间的联系,这样才能对集合的整体理解做到心中有数。我使用了归纳法和各种关系图,帮你理清思路,打通你的“任督二脉”。其中还有队列的内容,可能开发者经常会听到,但实际的工作中使用的较少,本部分内容也会带你玩转这些数据结构,让你在面试中能够应对自如。

第四部分:反射和动态代理

这部分会帮你理清反射和动态代理的关系,并提供很多实际使用的场景,让你更好地使用到反射和动态代理,当然这部分也会为你提供各种形式反射和动态代理的实现方式,让你可以随心所欲的使用它们。这部分内容还提供了
IO 的相关知识,并提供详细的示例和原理分析,也会试着编写一个简单 Socket 服务器。

第五部分:多线程编程

这部分包含 7 篇文章,讲述了包含 Java 8 在内的 8 种线程池,以及线程池的正确使用姿势,还有死锁代码的编写和死锁的解决方案。本部分还会介绍
Java 中的各种锁,以及它们的区别和使用场景,还会介绍 CAS 和著名的 ABA 问题的解决方案,还有多线程中的各种高频面试题。

第六部分:Java 热门框架和分布式消息队列

这部分包含 6 篇文章,从 Spring 到最近比较热门的微服务框架 SpringBoot,还有国内常用的 Mybatis 和 Java
技术栈中其他常用的框架,比如 Dubbo 和 Zookeeper,还有分布式消息队列 RabbitMQ 和 Kafka 的介绍和面试题汇总。

第七部分:Java 高手进阶

本部分内容包含 6 篇文章,可谓 Java 技术栈最实用的面试补充“大礼包”,有设计模式的面试题汇总;还有 DBA 级别的 MySQL 和 Redis
面试题汇总;还有成为高手必懂的 JVM 和算法的面试题汇总;最后回到本专栏的主题内容,提供了 Java
最容易出错的面试题汇总作为收束篇目,助你稳健地拿到想要的 Offer。

以下是这个专栏的知识树:

avatar

专栏寄语

希望通过本专栏的学习,你不但能拿到 Offer、取得更好的工作,还能建立一个完整的 Java
知识体系,让你学到的所有内容都能转化为实际的生产力,帮你在工作中取得不凡的成绩。并且希望明白原理后的你,能把这些记忆一直存储在自己大脑中,这样它将会成为你一辈子的财富。

我坚信:持续学习才是最有价值的投资,让我们一起行动起来,一起来做这件最有价值的事情。

最后,预祝每一位学习本专栏的朋友,都能找到一份自己理想中的工作。

Java 程序是如何执行的

了解任何一门语言的精髓都是先俯览其全貌,从宏观的视角把握全局,然后再深入每个知识点逐个击破,这样就可以深入而快速的掌握一项技能。同样学习 Java
也是如此,本节就让我们先从整体来看一下 Java 中的精髓。

Java 介绍

Java 诞生于 1991 年,Java 的前身叫做
Oak(橡树),但在注册商标的时候,发现这个名字已经被人注册了,后来团队的人就在咖啡馆讨论这件事该怎么办,有人灵机一动说叫 Java
如何,因为当时他们正在喝着一款叫做 Java 的咖啡。就这样,这个后来家喻户晓的名字,竟以这种“随意”的方式诞生了,并一直沿用至今。

Java 发展历程:

  • 1990,Sun 成立了“Green Team”项目小组
  • 1991,Java 语言前身 Oak(橡树)诞生
  • 1995,Oak 语言更名为 Java
  • 1996,Java 1.0 发布
  • 1997,Java 1.1 发布
  • 1998,Java 1.2 发布
  • 2000,Java 1.3 发布
  • 2000,Java 1.4 发布
  • 2004,Java 5 发布
  • 2006,Java 6 发布
  • 2011,Java 7 发布
  • 2014,Java 8 发布
  • 2017,Java 9(非长期支持版)发布
  • 2018.03,Java 10(非长期支持版) 发布
  • 2018.09,Java 11(长期支持版)发布
  • 2019.03, Java 12(非长期支持版) 发布

注:长期支持版指的是官方发布版本后的一段时间内,通常以“年”为计数单位,会对此版本进行持续维护和升级。

版本发布时间

Java 10 之后,官方表示每半年推出一个大版本,长期支持版本(LTS)每三年发布一次。

Java 和 JDK 的关系

JDK(Java Development Kit)Java 开发工具包,它包括:编译器、Java 运行环境(JRE,Java Runtime
Environment)、JVM(Java 虚拟机)监控和诊断工具等,而 Java 则表示一种开发语言。

Java 程序是怎么执行的?

我们日常的工作中都使用开发工具(IntelliJ IDEA 或 Eclipse 等)可以很方便的调试程序,或者是通过打包工具把项目打包成 jar 包或者
war 包,放入 Tomcat 等 Web 容器中就可以正常运行了,但你有没有想过 Java 程序内部是如何执行的?

其实不论是在开发工具中运行还是在 Tomcat 中运行,Java 程序的执行流程基本都是相同的,它的执行流程如下:

1. 先把 Java 代码编译成字节码,也就是把 .java 类型的文件编译成 .class 类型的文件。这个过程的大致执行流程:Java 源代码 -> 词法分析器 -> 语法分析器 -> 语义分析器 -> 字节码生成器 -> 最终生成字节码,其中任何一个节点执行失败就会造成编译失败;
2. 把 class 文件放置到 Java 虚拟机,这个虚拟机通常指的是 Oracle 官方自带的 Hotspot JVM;
3. Java 虚拟机使用类加载器(Class Loader)装载 class 文件;
4. 类加载完成之后,会进行字节码校验,字节码校验通过之后 JVM 解释器会把字节码翻译成机器码交由操作系统执行。但不是所有代码都是解释执行的,JVM 对此做了优化,比如,以 Hotspot 虚拟机来说,它本身提供了 JIT(Just In Time)也就是我们通常所说的动态编译器,它能够在运行时将热点代码编译为机器码,这个时候字节码就变成了编译执行。

Java 程序执行流程图如下:

avatar

Java 虚拟机是如何判定热点代码的?

Java 虚拟机判定热点代码的方式有两种:

  • 基于采样的热点判定

主要是虚拟机会周期性的检查各个线程的栈顶,若某个或某些方法经常出现在栈顶,那这个方法就是“热点方法”。这种判定方式的优点是实现简单;缺点是很难精确一个方法的热度,容易受到线程阻塞或外界因素的影响。

  • 基于计数器的热点判定

主要就是虚拟机给每一个方法甚至代码块建立了一个计数器,统计方法的执行次数,超过一定的阀值则标记为此方法为热点方法。

Hotspot 虚拟机使用的基于计数器的热点探测方法。它使用了两类计数器:方法调用计数器和回边计数器,当到达一定的阀值是就会触发 JIT 编译。

方法调用计数器:在 client 模式下的阀值是 1500 次,Server 是 10000 次,可以通过虚拟机参数:
-XX:CompileThreshold=N 对其进行设置。但是JVM还存在热度衰减,时间段内调用方法的次数较少,计数器就减小。

回边计数器:主要统计的是方法中循环体代码执行的次数。

由上面的知识我们可以看出, 要想做到对 Java 了如指掌,必须要好好学习 Java 虚拟机 ,那除了 Java
虚拟机外,还有哪些知识是面试必考,也是 Java 工程师必须掌握的知识呢?

1. Java 基础中的核心内容

字符串和字符串常量池的深入理解、Array 的操作和排序算法、深克隆和浅克隆、各种 IO 操作、反射和动态代理(JDK 自身动态代理和 CGLIB)等。

2. 集合

集合和 String
是编程中最常用的数据类型,关于集合的知识也是面试备考的内容,它包含:链表(LinkedList)、TreeSet、栈(Stack)、队列(双端、阻塞、非阻塞队列、延迟队列)、HashMap、TreeMap
等,它们的使用和底层存储数据结构都是热门的面试内容。

3. 多线程

多线程使用和线程安全的知识也是必考的面试题目,它包括:死锁、6
种线程池的使用与差异、ThreadLocal、synchronized、Lock、JUC(java.util.concurrent包)、CAS(Compare
and Swap)、ABA 问题等。

4. 热门框架

Spring、Spring MVC、MyBatis、SpringBoot

5. 分布式编程

消息队列(RabbitMQ、Kafka)、Dubbo、Zookeeper、SpringCloud 等。

6. 数据库

MySQL 常用引擎的掌握、MySQL 前缀索引、回表查询、数据存储结构、最左匹配原则、MySQL 的问题分析和排除方案、MySQL 读写分离的实现原理以及
MySQL 的常见优化方案等。 Redis 的使用场景、缓存雪崩和缓存穿透的解决方案、Redis 过期淘汰策略和主从复制的实现方案等。

7. Java 虚拟机

虚拟机的组成、垃圾回收算法、各种垃圾回收器的区别、Java 虚拟机分析工具的掌握、垃圾回收器的常用调优参数等。

8. 其他

常用算法的掌握、设计模式的理解、网络知识和常见 Linux 命令的掌握等。

值得庆幸的是以上所有内容都包含在本专栏中,接下来就让我们一起学习,一起构建 Java 的认知体系吧!

相关面试题

1. Java 语言都有哪些特点?

答:Java 语言包含以下特点。

  • 面向对象,程序容易理解、开发简单、方便;
  • 跨平台,可运行在不同服务器类型上,比如:Linux、Windows、Mac 等;
  • 执行性能好,运行效率高;
  • 提供大量的 API 扩展,语言强大;
  • 有多线程支持,增加了响应和实时交互的能力;
  • 安全性好,自带验证机制,确保程序的可靠性和安全性。
2. Java 跨平台实现的原理是什么?

答:要了解 Java 跨平台实现原理之前,必须先要了解 Java 的执行过程,Java 的执行过程如下:

执行过程

Java 执行流程:Java 源代码(.java)-> 编译 -> Java 字节码(.class) ->通过 JVM(Java 虚拟机)运行 Java
程序。每种类型的服务器都会运行一个 JVM,Java 程序只需要生成 JVM 可以执行的代码即可,JVM
底层屏蔽了不同服务器类型之间的差异,从而可以在不同类型的服务器上运行一套 Java 程序。

3. JDK、JRE、JVM 有哪些区别?

答:了解了 JDK、JRE、JVM 的定义也就明白了它们之间的区别,如下所述。

  • JDK:Java Development Kit(Java 开发工具包)的简称,提供了 Java 的开发环境和运行环境;
  • JRE:Java Runtime Environment(Java 运行环境)的简称,为 Java 的运行提供了所需环境;
  • JVM:Java Virtual Machine(Java虚拟机)的简称,是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的,简单来说就是所有的 Java 程序都是运行在 JVM(Java 虚拟机)上的。

总体来说,JDK 提供了一整套的 Java 运行和开发环境,通常使用对象为 Java 的开发者,当然 JDK 也包含了 JRE;而 JRE 为 Java
运行的最小运行单元,一般安装在 Java 服务器上,所以 JDK 和 JRE 可以从用途上进行理解和区分。JVM 不同于 JDK 和 JRE,JVM 是
Java 程序运行的载体,Java 程序只有通过 JVM 才能正常的运行。

4. Java 中如何获取明天此刻的时间?

答:JDK 8 之前使用 Calendar.add() 方法获取,代码如下:

Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DATE, 1);
System.out.println(calendar.getTime());

JDK 8 有两种获取明天时间的方法。

方法一,使用 LocalDateTime.plusDays() 方法获取,代码如下:

LocalDateTime today = LocalDateTime.now();
LocalDateTime tomorrow = today.plusDays(1);
System.out.println(tomorrow);

方法二,使用 LocalDateTime.minusDays() 方法获取,代码如下:

LocalDateTime today = LocalDateTime.now();
LocalDateTime tomorrow = today.minusDays(-1);
System.out.println(tomorrow);

minusDays() 方法为当前时间减去 n 天,传负值就相当于当前时间加 n 天。

5. Java 中如何跳出多重嵌套循环?

答:Java 中跳出多重嵌套循环的两种方式。

  • 方法一:定义一个标号,使用 break 加标号的方式
  • 方法二:使用全局变量终止循环

方法一,示例代码:

myfor:for (int i = 0; i < 100; i++) {
    for (int j = 0; j < 100; j++) {
        System.out.println("J:" + j);
        if (j == 10) {
            // 跳出多重循环
            break myfor;
        }
    }
}

方法二,示例代码:

boolean flag = true;
for (int i = 0; i < 100 && flag; i++) {
    for (int j = 0; j < 100; j++) {
        System.out.println("J:" + j);
        if (j == 10) {
            // 跳出多重循环
            flag = false;
            break;
        }
    }
}
6. char 变量能不能存贮一个中文汉字?为什么?

答:char 变量可以存贮一个汉字,因为 Java 中使用的默认编码是 Unicode ,一个 char 类型占 2 个字节(16
bit),所以放一个中文是没问题的。

7. Java 中会存在内存泄漏吗?请简单描述一下。

答:一个不再被程序使用的对象或变量一直被占据在内存中就造成了内存泄漏。

Java 中的内存泄漏的常见情景如下:

  • 长生命周期对象持有短生命的引用,比如,缓存系统,我们加载了一个对象放在缓存中,然后一直不使用这个缓存,由于缓存的对象一直被缓存引用得不到释放,就造成了内存泄漏;
  • 各种连接未调用关闭方法,比如,数据库 Connection 连接,未显性地关闭,就会造成内存泄漏;
  • 内部类持有外部类,如果一个外部类的实例对象的方法返回了一个内部类的实例对象,这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄露;
  • 改变哈希值,当一个对象被存储进 HashSet 集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了,否则对象修改后的哈希值与最初存储进 HashSet 集合中时的哈希值就不同了,在这种情况下,即使在 contains 方法使用该对象的当前引用作为的参数去 HashSet 集合中检索对象,也将返回找不到对象的结果,这也会导致无法从 HashSet 集合中单独删除当前对象,造成内存泄露。

你不知道的基本数据类型和包装类 + 面试题

基本数据类型

Java 基本数据按类型可以分为四大类:布尔型、整数型、浮点型、字符型,这四大类包含 8 种基本数据类型。

  • 布尔型:boolean
  • 整数型:byte、short、int、long
  • 浮点型:float、double
  • 字符型:char

8 种基本类型取值如下:

数据类型代表含义默认值取值包装类
boolean布尔型false0(false) 到 1(true)Boolean
byte字节型(byte)0﹣128 到 127Byte
char字符型‘\u0000’(空)‘\u0000’ 到 ‘\uFFFF’Character
short短整数型(short)0- 2 15 2^{15} 215 2 15 2^{15} 215
﹣1Short
int整数型0 2 31 2^{31} 231 2 31 2^{31} 231﹣1Integer
long长整数型0L 2 63 2^{63} 263 2 63 2^{63} 263﹣1Long
float单浮点型0.0f1.4e-45 到 3.4e+38Float
double双浮点型0.0d4.9e-324 到 1.798e+308Double

除 char 的包装类 Character 和 int 的包装类 Integer
之外,其他基本数据类型的包装类只需要首字母大写即可。包装类的作用和特点,本文下半部分详细讲解。

我们可以在代码中,查看某种类型的取值范围,代码如下:

public static void main(String[] args) {
    // Byte 取值:-128 ~ 127
    System.out.println(String.format("Byte 取值:%d ~ %d", Byte.MIN_VALUE, Byte.MAX_VALUE));
    // Int 取值:-2147483648 ~ 2147483647
    System.out.println(String.format("Int 取值:%d ~ %d", Integer.MIN_VALUE, Integer.MAX_VALUE));
}

包装类型

我们知道 8 种基本数据类型都有其对应的包装类,因为 Java
的设计思想是万物既对象,有很多时候我们需要以对象的形式操作某项功能,比如说获取哈希值(hashCode)或获取类(getClass)等。

那包装类特性有哪些?

1. 功能丰富

包装类本质上是一个对象,对象就包含有属性和方法,比如 hashCode、getClass 、max、min 等。

2. 可定义泛型类型参数

包装类可以定义泛型,而基本类型不行。

比如使用 Integer 定义泛型,代码:

List<Integer> list = new ArrayList<>();

如果使用 int 定义就会报错,代码:

List list = new ArrayList<>();  // 编译器代码报错

3. 序列化

因为包装类都实现了 Serializable 接口,所以包装类天然支持序列化和反序列化。比如 Integer 的类图如下:

Integer 类图

4. 类型转换

包装类提供了类型转换的方法,可以很方便的实现类型之间的转换,比如 Integer 类型转换代码:

String age = "18";
int ageInt = Integer.parseInt(age) + 2;
// 输出结果:20
System.out.println(ageInt);

5. 高频区间的数据缓存

此特性为包装类很重要的用途之一,用于高频区间的数据缓存,以 Integer 为例来说,在数值区间为 -128~127
时,会直接复用已有对象,在这区间之外的数字才会在堆上产生。

我们使用 == 对 Integer 进行验证,代码如下:

public static void main(String[] args) {
        // Integer 高频区缓存范围 -128~127
        Integer num1 = 127;
        Integer num2 = 127;
        // Integer 取值 127 == 结果为 true(值127 num1==num2 => true)
        System.out.println("值127 num1==num2 => " + (num1 == num2));
        Integer num3 = 128;
        Integer num4 = 128;
        // Integer 取值 128 == 结果为 false(值128 num3==num4 => false)
        System.out.println("值128 num3==num4 => " + (num3 == num4));
    }

从上面的代码很明显可以看出,Integer 为 127 时复用了已有对象,当值为 128 时,重新在堆上生成了新对象。

为什么会产生高频区域数据缓存?我们查看源码就能发现“线索”,源码版本 JDK8,源码如下:

public static Integer valueOf(int i) {
  if (i >= IntegerCache.low && i <= IntegerCache.high)
    return IntegerCache.cache[i + (-IntegerCache.low)];
  return new Integer(i);
}

由此可见,高频区域的数值会直接使用已有对象,非高频区域的数值会重新 new 一个新的对象。

各包装类高频区域的取值范围:

  • Boolean:使用静态 final 定义,就会返回静态值
  • Byte:缓存区 -128~127
  • Short:缓存区 -128~127
  • Character:缓存区 0~127
  • Long:缓存区 -128~127
  • Integer:缓存区 -128~127

包装类的注意事项

  • int 的默认值是 0,而 Integer 的默认值是 null。

  • 推荐所有包装类对象之间的值比较使用 equals() 方法,因为包装类的非高频区数据会在堆上产生,而高频区又会复用已有对象,这样会导致同样的代码,因为取值的不同,而产生两种截然不同的结果。代码示例:

    public static void main(String[] args) {
    // Integer 高频区缓存范围 -128~127
    Integer num1 = 127;
    Integer num2 = 127;
    // Integer 取值 127 == 结果为 true(值127 num1num2 => true)
    System.out.println("值127 num1
    num2 => " + (num1 == num2));
    Integer num3 = 128;
    Integer num4 = 128;
    // Integer 取值 128 == 结果为 false(值128 num3num4 => false)
    System.out.println("值128 num3
    num4 => " + (num3 == num4));
    // Integer 取值 128 equals 结果为 true(值128 num3.equals(num4) => true)
    System.out.println("值128 num3.equals(num4) => " + num3.equals(num4));
    }

  • Float 和 Double 不会有缓存,其他包装类都有缓存。

  • Integer 是唯一一个可以修改缓存范围的包装类,在 VM optons 加入参数:

-XX:AutoBoxCacheMax=666 即修改缓存最大值为 666

示例代码如下:

public static void main(String[] args) {
    Integer num1 = -128;
    Integer num2 = -128;
    System.out.println("值为-128 => " + (num1 == num2));
    Integer num3 = 666;
    Integer num4 = 666;
    System.out.println("值为666 => " + (num3 == num4));
    Integer num5 = 667;
    Integer num6 = 667;
    System.out.println("值为667 => " + (num5 == num6));
}

执行结果如下:

值为-128 => true
值为666 => true
值为667 => false

由此可见将 Integer 最大缓存修改为 666 之后,667 不会被缓存,而 -128~666 之间的数都被缓存了。

相关面试题

1. 以下 Integer 代码输出的结果是?

Integer age = 10;
Integer age2 = 10;
Integer age3 = 133;
Integer age4 = 133;
System.out.println((age == age2) + "," + (age3 == age4));

答:true,false

2. 以下 Double 代码输出的结果是?

Double num = 10d;
Double num2 = 10d;
Double num3 = 133d;
Double num4 = 133d;
System.out.println((num == num2) + "," + (num3 == num4));

答:false,false

3. 以下程序输出结果是?

int i = 100;
Integer j = new Integer(100);
System.out.println(i == j);
System.out.println(j.equals(i));

A:true,true
B:true,false
C:false,true
D:false,false

答:A

题目分析:有人认为这和 Integer 高速缓存有关系,但你发现把值改为 10000 结果也是 true,true,这是因为 Integer 和 int
比较时,会自动拆箱为 int 相当于两个 int 比较,值一定是 true,true

4. 以下程序执行的结果是?

final int iMax = Integer.MAX_VALUE;
System.out.println(iMax + 1);

A:2147483648
B:-2147483648
C:程序报错
D:以上都不是

答:B

题目解析:这是因为整数在内存中使用的是补码的形式表示,最高位是符号位 0 表示正数,1 表示负数,当执行 +1 时,最高位就变成了 1,结果就成了
-2147483648。

5. 以下程序执行的结果是?

Set<Short> set = new HashSet<>();
for (short i = 0; i < 5; i++) {
    set.add(i);
    set.remove(i - 1);
}
System.out.println(set.size());

A:1
B:0
C:5
D:以上都不是

答:5

题目解析:Short 类型 -1 之后转换成了 Int 类型,remove() 的时候在集合中找不到 Int
类型的数据,所以就没有删除任何元素,执行的结果就是 5。

6. short s=2;s=s+1; 会报错吗?short s=2;s+=1; 会报错吗?

答:s=s+1 会报错,s+=1 不会报错,因为 s=s+1 会导致 short 类型升级为 int 类型,所以会报错,而 s+=1 还是原来的 short
类型,所以不会报错。

7. float f=3.4; 会报错吗?为什么?

答:会报错,因为值 3.4 是 double 类型,float 类型级别小于 double 类型,所以会报错。如下图所示:

报错示例图

8. 为什么需要包装类?

答:需要包装类的原因有两个。

① Java 的设计思想是万物既对象,包装类体现了面向对象的设计理念;
② 包装类包含了很多属性和方法,比基本数据类型功能多,比如提供的获取哈希值(hashCode)或获取类(getClass)的方法等。

9. 基本类 int 和包装类 Integer,在 -128~127 之间都会复用已有的缓存对象,这种说法正确吗?

答:不正确,只有包装类高频区域数据才有缓存。

10. 包装类 Double 和 Integer 一样都有高频区域数据缓存,这种说法正确吗?

答:不正确,基本数据类型的包装类只有 Double 和 Float 没有高频区域的缓存。

11. 包装类的值比较要使用什么方法?

答:包装类因为有高频区域数据缓存,所以推荐使用 equals() 方法进行值比较。

12. 包装类有哪些功能?

答:包装类提供的功能有以下几个。

  • 功能丰富:包装类包含了有 hashCode、getClass 、max、min 等方法;
  • 可定义泛型类型参数:例如 List<Integer> list = new ArrayList<>(); ;
  • 序列化:包装类实现了 Serializable 接口,所以包装类天然支持序列化和反序列化;
  • 类型转换:包装类提供了方便的类型转换方法,比如 Integer 的 parseInt() 方法;
  • 高频区域数据缓存:高频区域可使用已有的缓存对象。

详见正文“包装类型”部分内容。

13. 泛型可以为基本类型吗?为什么?

答:泛型不能使用基本数据类型。泛型在 JVM(Java虚拟机)编译的时候会类型檫除,比如代码 List<Integer> list 在 JVM
编译的时候会转换为 List list ,因为泛型是在 JDK 5 时提供的,而 JVM
的类型檫除是为了兼容以前代码的一个折中方案,类型檫除之后就变成了 Object,而 Object
不能存储基本数据类型,但可以使用基本数据类型对应的包装类,所以像 List<int> list 这样的代码是不被允许的,编译器阶段会检查报错,而
List<Integer> list 是被允许的。

14. 选择包装类还是基本类的原则有哪些?

答:我们知道正确的使用包装类,可以提供程序的执行效率,可以使用已有的缓存,一般情况下选择基本数据类型还是包装类原则有以下几个。

① 所有 POJO 类属性必须使用包装类;
② RPC 方法返回值和参数必须使用包装类;
③ 所有局部变量推荐使用基本数据类型。

15. 基本数据类型在 JVM 中一定存储在栈中吗?为什么?

答:基本数据类型不一定存储在栈中,因为基本类型的存储位置取决于声明的作用域,来看具体的解释。

  • 当基本数据类型为局部变量的时候,比如在方法中声明的变量,则存放在方法栈中的,当方法结束系统会释放方法栈,在该方法中的变量也会随着栈的销毁而结束,这也是局部变量只能在方法中使用的原因;
  • 当基本数据类型为全局变量的时候,比如类中的声明的变量,则存储在堆上,因为全局变量不会随着某个方法的执行结束而销毁。
16. 以下程序执行的结果是?

Integer i1 = new Integer(10);
Integer i2 = new Integer(10);
Integer i3 = Integer.valueOf(10);
Integer i4 = Integer.valueOf(10);
System.out.println(i1 == i2);
System.out.println(i2 == i3);
System.out.println(i3 == i4);

A:false,false,false
B:false,false,true
C:false,true,true
D:true,false,false

答:B

题目解析:new Integer(10) 每次都会创建一个新对象,Integer.valueOf(10) 则会使用缓存池中的对象。

17. 3*0.1==0.3 返回值是多少?

答:返回值为:false。

题目解析:因为有些浮点数不能完全精确的表示出来,如下代码:

System.out.println(3 * 0.1);

返回的结果是:0.30000000000000004。


深入理解字符串 + 面试题

字符串介绍

字符串是程序开发当中,使用最频繁的类型之一,有着与基础类型相同的地位,甚至在 JVM(Java
虚拟机)编译的时候会对字符串做特殊的处理,比如拼加操作可能会被 JVM 直接合成为一个最终的字符串,从而到达高效运行的目的。

1 String 特性
  • String 是标准的不可变类(immutable),对它的任何改动,其实就是创建了一个新对象,再把引用指向该对象;
  • String 对象赋值之后就会在常量池中缓存,如果下次创建会判定常量池是否已经有缓存对象,如果有的话直接返回该引用给创建者。
2 字符串创建

字符串创建的两种方式:

  • String str = “laowang”;
  • String str = new String(“laowang”);
3 注意事项

查看下面代码:

String s1 = "laowang";
String s2 = s1;
String s3 = new String(s1);
System.out.println(s1 == s2);
System.out.println(s1 == s3);

输出结果:true、false

为什么会这样?原因是 s3 使用 new String 时一定会在堆中重新创建一个内存区域,而 s2 则会直接使用了 s1
的引用,所以得到的结果也完全不同。

字符串的使用

1 字符串拼加

字符串拼加的几种方式:

  • String str = “lao” + “wang”;
  • String str = “lao”; str += “wang”;
  • String str = “lao”; String str2 = str + “wang”;
2 JVM 对字符串的优化

根据前面的知识我们知道,对于 String 的任何操作其实是创建了一个新对象,然后再把引用地址返回该对象,但 JVM 也会对 String
进行特殊处理,以此来提供程序的运行效率,比如以下代码:

String str = "hi," + "lao" + "wang";

经过 JVM 优化后的代码是这样的:

String str = "hi,laowang";

验证代码如下:

String str = "hi," + "lao" + "wang";
String str2 = "hi,laowang";
System.out.println(str == str2);

执行的结果:true

这就说明 JVM 在某些情况下会特殊处理 String 类型。

3 字符串截取

字符串的截取使用 substring() 方法,使用如下:

String str = "abcdef";
// 结果:cdef(从下标为2的开始截取到最后,包含开始下标)
System.out.println(str.substring(2));
// 结果:cd(从下标为2的开始截取到下标为4的,包含开始下标不包含结束下标)
System.out.println(str.substring(2,4));
4 字符串格式化

字符串格式化可以让代码更简洁更直观,比如,“我叫老王,今年 30
岁,喜欢读书”在这条信息中:姓名、年龄、兴趣都是要动态改变的,如果使用“+”号拼接的话很容易出错,这个时候字符串格式化方法 String.format()
就派上用场了,代码如下:

String str = String.format("我叫%s,今年%d岁,喜欢%s", "老王", 30, "读书");

转换符说明列表:

转换符说明
%s字符串类型
%d整数类型(十进制)
%c字符类型
%b布尔类型
%x整数类型(十六进制)
%o整数类型(八进制)
%f浮点类型
%a浮点类型(十六进制)
%e指数类型
%%百分比类型
%n换行符
5 字符对比

根据前面的知识我们知道,使用 String 和 new String 声明的对象是不同的,那有没有简单的方法,可以忽略它们的创建方式(有没有
new)而只对比它们的值是否相同呢?答案是肯定的,使用 equals() 方法可以实现,代码如下:

String s1 = "hi," + "lao" + "wang";
String s2 = "hi,";
s2 += "lao";
s2 += "wang";
String s3 = "hi,laowang";
System.out.println(s1.equals(s2)); // true
System.out.println(s1.equals(s3)); // true
System.out.println(s2.equals(s3)); // true

以上使用 equals 对比的结果都为 true

如果要忽略字符串的大小写对比值可以使用 equalsIgnoreCase(),代码示例:

String s1 = "Hi,laowang";
String s2 = "hi,laowang";
System.out.println(s1.equals(s2)); // false
System.out.println(s1.equalsIgnoreCase(s2)); // true

s1.equals(s2) 执行的结果为:false,s1.equalsIgnoreCase(s2) 执行的结果为:true

6 String、StringBuffer、StringBuilder

字符串相关类型主要有这三种:String、StringBuffer、StringBuilder,其中 StringBuffer、StringBuilder
都是可以变的字符串类型,StringBuffer 在字符串拼接时使用 synchronized 来保障线程安全,因此在多线程字符串拼接中推荐使用
StringBuffer。

StringBuffer 使用:

StringBuffer sf = new StringBuffer("lao");
// 添加字符串到尾部
sf.append("wang"); // 执行结果:laowang
// 插入字符串到到当前字符串下标的位置
sf.insert(0,"hi,"); // 执行结果:hi,laowang
// 修改字符中某个下标的值
sf.setCharAt(0,'H'); // 执行结果:Hi,laowang

StringBuilder 的使用方法和 StringBuffer 一样,它们都继承于 AbstractStringBuilder。

相关面试题

1. String 属于基础数据类型吗?

答:String 不是基础数据类型,它是从堆上分配来的。基础数据类型有 8
个,分别为:boolean、byte、short、int、long、float、double、char。

2. 以下可以正确获取字符串长度的是?

A:str.length
B:str.size
C:str.length()
D:str.size()

答:C

题目解析:字符串没有 length 属性,只有 length() 方法。

3. “==” 和 equals 的区别是什么?

答:“==” 对基本类型来说是值比较,对于引用类型来说是比较的是引用;而 equals 默认情况下是引用比较,只是很多类重写了 equals 方法,比如
String、Integer 等把它变成了值比较,所以一般情况下 equals 比较的是值是否相等。

① “==” 解读

对于基本类型和引用类型 == 的作用效果是不同的,如下所示:

  • 基本类型:比较的是值是否相同;
  • 引用类型:比较的是引用是否相同。

代码示例:

String x = "string";
String y = "string";
String z = new String("string");
System.out.println(x==y); // true
System.out.println(x==z); // false
System.out.println(x.equals(y)); // true
System.out.println(x.equals(z)); // true

代码说明:因为 x 和 y 指向的是同一个引用,所以 == 也是 true,而 new String() 方法则重写开辟了内存空间,所以 ==
结果为 false,而 equals 比较的一直是值,所以结果都为 true。

② equals 解读

equals 本质上就是 ==,只不过 String 和 Integer 等重写了 equals 方法,把它变成了值比较。看下面的代码就明白了。

首先来看默认情况下 equals 比较一个有相同值的对象,代码如下:

class Cat {
    public Cat(String name) {
        this.name = name;
    }
    private String name;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}
Cat c1 = new Cat("王磊");
Cat c2 = new Cat("王磊");
System.out.println(c1.equals(c2)); // false

输出结果出乎我们的意料,竟然是 false?!

这是怎么回事,看了 equals 源码就知道了,源码如下:

public boolean equals(Object obj) {
    return (this == obj);
}

原来 equals 本质上就是 ==

那问题来了,两个相同值的 String 对象,为什么返回的是 true?代码如下:

String s1 = new String("老王");
String s2 = new String("老王");
System.out.println(s1.equals(s2)); // true

同样的,当我们进入 String 的 equals 方法,找到了答案,代码如下:

public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = value.length;
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

原来是 String 重写了 Object 的 equals 方法,把引用比较改成了值比较。

总结来说,“==” 对于基本类型来说是值比较,对于引用类型来说是比较的是引用;而 equals 默认情况下是引用比较,只是很多类重写了 equals
方法,比如 String、Integer 等把它变成了值比较,所以一般情况下 equals 比较的是值是否相等。

4. 以下代码输出的结果是?

String str = "laowang";
str.substring(0,1);
System.out.println(str);

A:l
B:a
C:la
D:laowang

答:D

题目解析:因为 String 的 substring() 方法不会修改原字符串内容,所以结果还是 laowang。

5. 以下字符串对比的结果是什么?

String s1 = "hi," + "lao" + "wang";
String s2 = "hi,";
s2 += "lao";
s2 += "wang";
String s3 = "hi,laowang";
System.out.println(s1 == s2);
System.out.println(s1 == s3);
System.out.println(s2 == s3);

答:false true false

题目解析:String s1 = “hi,” + “lao” + “wang” 代码会被 JVM 优化为:String s1 =
“hi,laowang”,这样就和 s3 完全相同,s1 创建的时候会把字符"hi,laowang"放入常量池,s3
创建的时候,常量池中已经存在对应的缓存,会直接把引用返回给 s3,所以 s1==s3 就为 true,而 s2 使用了 +=
其引用地址就和其他两个不同。

6. 以下 String 传值修改后执行的结果是什么?

public static void main(String[] args) {
  String str = new String("laowang");
  change(str);
  System.out.println(str);
}
public static void change(String str) {
    str = "xiaowang";
}

答:laowang

7. 以下 StringBuffer 传值修改后的执行结果是什么?

public static void main(String[] args) {
  StringBuffer sf = new StringBuffer("hi,");
  changeSf(sf);
  System.out.println(sf);
}
public static void changeSf(StringBuffer sf){
    sf.append("laowang");
}

答:hi,laowang

题目解析:String 为不可变类型,在方法内对 String 修改的时候,相当修改传递过来的是一个 String 副本,所以 String
本身的值是不会被修改的,而 StringBuffer 为可变类型,参数传递过来的是对象的引用,对其修改它本身就会发生改变。

8. 以下使用 substring 执行的结果什么?

String str = "abcdef";
System.out.println(str.substring(3, 3));

答:“”(空)。

9. 判定字符串是否为空,有几种方式?

答:常用的方式有以下两种。

  • str.equals(“”)
  • str.length()==0
10. String、StringBuffer、StringBuilder 的区别?

答:以下是 String、StringBuffer、StringBuilder 的区别:

  • 可变性:String 为字符串常量是不可变对象,StringBuffer 与 StringBuilder 为字符串变量是可变对象;
  • 性能:String 每次修改相当于生成一个新对象,因此性能最低;StringBuffer 使用 synchronized 来保证线程安全,性能优于 String,但不如 StringBuilder;
  • 线程安全:StringBuilder 为非线程安全类,StringBuffer 为线程安全类。
11. String 对象的 intern() 有什么作用?

答:intern() 方法用于查找常量池中是否存在该字符值,如果常量池中不存在则先在常量池中创建,如果已经存在则直接返回。

示例代码:

String s = "laowang";
String s2 = s.intern();
System.out.println(s == s2); // 返回 true
12. String s=new String(“laowang”) 创建了几个对象?

答:总共创建了两个对象,一个是字符串 “laowang”,另一个是指向字符串的变量 s。new String()
不管常量池有没有相同的字符串,都会在内存(非字符串常量池)中创建一个新的对象。

13. 什么是字符串常量池?

字符串常量池是存储在 Java 堆内存中的字符串池,是为防止每次新建字符串带的时间和空间消耗的一种解决方案。在创建字符串时 JVM
会首先检查字符串常量池,如果字符串已经存在池中,就返回池中的实例引用,如果字符串不在池中,就会实例化一个字符串放到池中并把当前引用指向该字符串。

14. String 不可变性都有哪些好处?

答:不可变的好处如下。

  • 只有当字符串是不可变的,字符串常量池才能实现,字符串池的实现可以在运行时节约很多堆空间,因为不同的字符串变量都指向池中的同一个字符串;
  • 可以避免一些安全漏洞,比如在 Socket 编程中,主机名和端口都是以字符串的形式传入,因为字符串是不可变的,所以它的值是不可改变的,否则黑客们可以钻到空子,改变字符串指向的对象的值,造成安全漏洞;
  • 多线程安全,因为字符串是不可变的,所以同一个字符串实例可以被多个线程共享,保证了多线程的安全性;
  • 适合做缓存的 key,因为字符串是不可变的,所以在它创建的时候哈希值就被缓存了,不需要重新计算速度更快,所以字符串很适合作缓存的中的 key。
15. String 是否可以被继承?为什么?

答:String 不能被继承。因为 String 被声明为 final(最终类),所以不能被继承,源码如下(JDK 8)。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    //......
}

Java 中的运算符和流程控制 + 面试题

算术运算符

Java 中的算法运算符,包括以下几种:

算术运算符名称举例
  • | 加法 | 1+2=3
  • | 减法 | 2-1=1
  • | 乘法 | 2*3=6
    / | 除法 | 24/8=3
    % | 求余 | 24%7=3
    ++ | 自增1 | int i=1;i++
    – | 自减1 | int i=1;i–

我们本讲要重点讲的是 “++” 和 “–”,其他的算术运算符相对比较简单直观,本讲就不花精力去讲解了,之所以要把 “++” 和 “–”
单独拿出来讲,是因为在使用他们的时候有很多坑需要开发者注意,最重要的是 “++” 和 “–” 也是面试中高频出现的面试题。

先来看 “++” 的基本使用:

int i = 1;
int i2 = ++i; // ++i 相当于 i = 1+i;
System.out.println(i);  // 2
System.out.println(i2); // 2

++ii++ 的区别

  • ++i 先自加再赋值
  • i++ 先赋值再自加

比如:

int i = 0;
int i2 = i++;
int j = 0;
int j2 = ++j;
System.out.println("i2=" + i2);
System.out.println("j2=" + j2);

输出的结果:

i2=0
j2=1

代码解析:i++ 是先给 i2 赋值再自身 +1 ,所以 i2 等于0,而 ++j 是先自加等于 1 之后,再赋值给 j2,所以 j2 等于 1。

注意事项

++/-- 是非线程安全的,也就是说 ++/-- 操作在多线程下可能会引发混乱,例如下面代码:

new Thread() {
    @Override
    public void run() {
        for (int i = 0; i < 100000; i++) {
            System.out.println("thread:" + this.getName() + ",count=" + (++count));
        }
    }
}.start();
new Thread() {
    @Override
    public void run() {
        for (int i = 0; i < 100000; i++) {
            System.out.println("thread:" + this.getName() + ",count=" + (++count));
        }
    }
}.start();

执行的结果,如下图:

执行结果

如上图所示,每台机器的执行可能略有差距,但大多数情况下并不能给我们想要的真实值 200000。

原理分析

“++” 操作在多线程下引发混乱的原因:因为 ++ 操作对于底层操作系统来说,并不是一条 CPU 操作指令,而是三条 CPU
操作指令——取值、累加、存储,因此无法保证原子性,就会出现上面代码执行后的误差。

如何避免 ++/-- 操作在多线程下的“误差”?

  • 方法一:++/-- 操作放在同步块 synchronized 中。
  • 方法二:自己申明锁,把 ++/-- 操作放入其中。
  • 方法三:使用 AtomicInteger 类型替代 int 类型。

最后,因为 – 的语法和 ++ 完全一致,所以 – 的操作,请参照上面的 ++ 语法。

条件运算符(三元运算符)

条件运算符(?:)也叫“三元运算符”。

语法:

布尔表达式 ? 表达式1 :表达式2

运算过程:如果布尔表达式的值为 true,则返回 表达式 1 的值 ,否则返回 表达式 2 的值

例如:

String s = 3 > 1 ? "三大于一" : "三小于一";
System.out.println(s);

执行结果:三大于一

流程控制

在 Java 语言中使用条件语句和循环结构来实现流程控制。

1 条件语句

条件语句的语法格式:

if(…) …

其中的条件判断必须使用括号括起来不能省略。

基础用法使用:

int i = 1;
if (i > 1) {
    System.out.println("i大于一");
} else if (i == 1) {
    System.out.println("i等于一");
} else {
    System.out.println("其他");
}
2 循环

while 当条件成立的时候执行下一条语句。

while 语法格式:

while(…) …

基本语法使用:

int i = 0;
  while (i < 3) {
  System.out.println(++i);
}

while 是先判断再决定是否执行,有可能一次也不执行,如果希望至少执行一次,可以使用 do/while。

do/while 语法格式:

do{…}while(…);

基本语法使用:

int i = 0;
do {
  System.out.println(++i);
} while (i < 3);
3 确定循环

for 循环是程序中最长使用的循环之一,它是利用每次迭代之后更新计数器来控制循环的次数。

for 语法格式:

for(int i=0;i<n;i++){ … }

基础语法使用:

for (int i = 0; i < 10; i++) {
    System.out.println("i=" + i);
}

for 循环中可使用关键字 continue,跳过后续操作,继续下一次迭代。

例如:

for (int i = 1; i < 4; i++) {
    if (i == 2) continue;
    System.out.println("i=" + i);
}

执行结果:

i=1
i=3

如结果所示,第二次循环就会跳过,执行下一次循环。

for 注意事项

在循环中检查两个浮点数是否相等要格外小心,例如下面代码:

public static void main(String[] args) {
    for (float i = 0; i != 1; i += 0.1) {
        System.out.println(i);
    }
}

循环永远不会停下来,由于舍入误差,因为 0.1 无法精确的用二级制表示,所以上面代码到 0.9000001 之后,会直接跳到 1.0000001,不会等于
1,所以循环就永远不会停下来。

4 多重选择

switch 的特点是可以判断多个条件,if 的特点是执行少量判断,它们两个刚好形成互补的关系。

switch 语法格式:

switch(…){ case 1: … break; … default: … break; }

switch 基础使用:

int i = 3;
switch (i) {
    case 1:
        System.out.println("等于1");
        break;
    case 2:
        System.out.println("等于2");
        break;
    case 3:
        System.out.println("等于3");
        break;
    default:
        System.out.println("等于其他");
        break;
}

可用于 case 的类型有:

  • byte、char、short、int
  • 枚举
  • 字符串(Java SE 7 新加入)

switch 注意事项

switch 使用时,每个选项最末尾一定不要忘记加 break 关键字,否则会执行多个条件。

案例:

int i = 1;
switch (i) {
    case 1:
        System.out.println("等于1");
    case 2:
        System.out.println("等于2");
    case 3:
        System.out.println("等于3");
    default:
        System.out.println("等于其他");
}

程序执行的结果:

等于1
等于2
等于3
等于其他

所以使用 switch 时,每个选项的末尾一定得加 break 关键字。

相关面试题

1. Java 中 i++ 和 ++i 有什么区别?

答:i 先赋值再运算;i 先运算再赋值。

示例代码:

int i = 0;
int i2 = i++;
int j = 0;
int j2 = ++j;
System.out.println("i2=" + i2);
System.out.println("j2=" + j2);

输出结果:i2=0,j2=1

2. 以下代码 i 的值是多少?

int i = 0;
i = i++;
System.out.println(i);

答:i=0

题目解析:因为 Java 虚拟机在执行 i++ 时,把这个值有赋值给了 i,而 i++ 是先赋值再相加,所以这个时候 i 接收到的结果自然是 0 了。

3. 以下代码 i2 和 i3 的值分别为多少?

int i = 0;
int i2 = i++;
int i3 = ++i;

答:i2=0,i3=2

4. 以下代码能不能正常执行?

if (true) System.out.println("laowang");

答:可以正常执行,其中判断条件的括号不能省略,大括号是可以省略的(作者并不建议为了省代码的而牺牲代码的可读性)。

5. 以下 switch 执行的结果是什么?

int num = 1;
switch (num) {
    case 0:
        System.out.print("0");
    case 1:
        System.out.print("1");
    case 2:
        System.out.print("2");
    case 3:
        System.out.print("3");
    default:
        System.out.print("default");
}

答:123default

6. switch 能否用于 byte 类型的判断上?能否用于 long 类型的判断上?

答:switch 支持 byte 类型的判断,不支持 long 类型的判断。

题目解析:switch 支持的全部类型(JDK
8):char、byte、short、int、Charachter、Byte、Short、Integer、String、enum。

7. while 必须配合 break 一起使用的说法正确吗?

答:错误,while 可以单独使用。

例如:

int i = 0;
while (i < 3) {
    System.out.println(++i);
}
8. 以下代码可以正常运行吗?为什么?

int i = 0;
while (i < 3) {
    if (i == 2) {
        return;
    }
    System.out.println(++i);
}

答:可以正常运行,这里的 return 和 break 的效果是一致的,while 可以配合 return 或 break 一起使用。

9. 以下的程序执行结果什么?

int i = 0;
do {
  System.out.println(++i);
} while (i < 3)

答:编译器报错,do/while 之后必须使用分号 ; 结尾。

10. 以下程序输出的结果是?

String s = new String("laowang");
String s2 = new String("laowang");
System.out.println(s == s2);
switch (s) {
    case "laowang":
        System.out.println("laowang");
        break;
    default:
        System.out.println("default");
        break;
}

A:true,default
B:false,default
C:false,laowang
D:true,laowang

答:C

11. 以下代码循环执行了几次?

for (float i = 0; i != 10; i += 0.1) {
    System.out.println("hi");
}

答:无数次,循环永远不会停下来。由于舍入误差,因为 0.1 无法精确的用二级制表示,所以上面代码到 0.9000001 之后,会直接跳到
1.0000001,不会等于 1,所以循环就永远不会停下来。

12. 以下代码输出的结果是?

int num = -4;
System.out.println(num % 2 == 1 || num % 2 == -1);

A:1
B:-1
C:true
D:false

答:D

题目解析:-4 % 2 = 0 既不等于 1 也不等于 -1,所以结果为 false。

13. 以下代码输出的结果是?

int num = 4;
num = ((num & 1) == 1);
System.out.println(num);

A:4
B:1
C:以上都不是

答:C

题目解析:== 运算返回的是 boolean 类型,不能使用 int 接收,所以程序会报错。


深入了解 Java 中的异常处理 + 面试题

在程序开发中,异常处理也是我们经常使用到的模块,只是平常很少去深究异常模块的一些知识点。比如,try-catch 处理要遵循的原则是什么,finally
为什么总是能执行,try-catch
为什么比较消耗程序的执行性能等问题,我们本讲内容都会给出相应的答案,当然还有面试中经常被问到的异常模块的一些面试题,也是我们本篇要讲解的重点内容。

异常处理基础介绍

先来看看 异常处理的语法格式

try{ … } catch(Exception e){ … } finally{ … }

其中,

  • try :是用来监测可能会出现异常的代码段。
  • catch :是用来捕获 try 代码块中某些代码引发的异常,如果 try 里面没有异常发生,那么 catch 也一定不会执行。在 Java 语言中,try 后面可以有多个 catch 代码块,用来捕获不同类型的异常,需要注意的是前面的 catch 捕获异常类型一定不能包含后面的异常类型,这样的话,编译器会报错。
  • finally :不论 try-catch 如何执行,finally 一定是最后执行的代码块,所有通常用来处理一些资源的释放,比如关闭数据库连接、关闭打开的系统资源等。

异常处理的基本使用 ,具体可以参考下面的代码段:

try {
    int i = 10 / 0;
} catch (ArithmeticException e) {
    System.out.println(e);
} finally {
    System.out.println("finally");
}

多 catch 的使用 ,具体可以参考下面的代码段:

try {
    int i = Integer.parseInt(null);
} catch (ArithmeticException ae) {
    System.out.println("ArithmeticException");
} catch (NullPointerException ne) {
    System.out.println("NullPointerException");
} catch (Exception e) {
    System.out.println("Exception");
}

需要注意的是 Java 虚拟机会从上往下匹配错误类型,因此前面的 catch 异常类型不能包含后面的异常类型。比如上面的代码如果把 Exception
放在最前面编译器就会报错,具体可以参考下面的图片。

enter image description
here

异常处理的发展

随着 Java 语言的发展,JDK 7 的时候引入了一些更加便利的特性,用来更方便的处理异常信息,如 try-with-resources 和
multiple catch,具体可以参考下面的代码段:

try (FileReader fileReader = new FileReader("");
     FileWriter fileWriter = new FileWriter("")) { // try-with-resources
    System.out.println("try");
} catch (IOException | NullPointerException e) { // multiple catch
    System.out.println(e);
}

异常处理的基本原则

先来看下面这段代码,有没有发现一些问题?

try {
  // ...
  int i = Integer.parseInt(null);
} catch (Exception e) {
}

以上的这段代码,看似“正常”,却违背了异常处理的两个基本原则:

  • 第一,尽量不要捕获通用异常,也就是像 Exception 这样的异常,而是应该捕获特定异常,这样更有助于你发现问题;
  • 第二,不要忽略异常,像上面的这段代码只是加了 catch,但没有进行如何的错误处理,信息就已经输出了,这样在程序出现问题的时候,根本找不到问题出现的原因,因此要切记不要直接忽略异常。

异常处理对程序性能的影响

异常处理固然好用,但一定不要滥用,比如下面的代码片段:

// 使用 com.alibaba.fastjson
JSONArray array = new JSONArray();
String jsonStr = "{'name':'laowang'}";
try {
    array = JSONArray.parseArray(jsonStr);
} catch (Exception e) {
    array.add(JSONObject.parse(jsonStr));
}
System.out.println(array.size());

这段代码是借助了 try-catch 去处理程序的业务逻辑,通常是不可取的,原因包括下列两个方面。

  • try-catch 代码段会产生额外的性能开销,或者换个角度说,它往往会影响 JVM 对代码进行优化,因此建议仅捕获有必要的代码段,尽量不要一个大的 try 包住整段的代码;与此同时,利用异常控制代码流程,也不是一个好主意,远比我们通常意义上的条件语句(if/else、switch)要低效。
  • Java 每实例化一个 Exception,都会对当时的栈进行快照,这是一个相对比较重的操作。如果发生的非常频繁,这个开销可就不能被忽略了。

以上使用 try-catch 处理业务的代码,可以修改为下列代码:

// 使用 com.alibaba.fastjson
JSONArray array = new JSONArray();
String jsonStr = "{'name':'laowang'}";
if (null != jsonStr && !jsonStr.equals("")) {
    String firstChar = jsonStr.substring(0, 1);
    if (firstChar.equals("{")) {
        array.add(JSONObject.parse(jsonStr));
    } else if (firstChar.equals("[")) {
        array = JSONArray.parseArray(jsonStr);
    }
}
System.out.println(array.size());

相关面试题

1. try 可以单独使用吗?

答:try 不能单独使用,否则就失去了 try 的意义和价值。

2. 以下 try-catch 可以正常运行吗?

try {
    int i = 10 / 0;
} catch {
    System.out.println("last");
}

答:不能正常运行,catch 后必须包含异常信息,如 catch (Exception e)。

3. 以下 try-finally 可以正常运行吗?

try {
    int i = 10 / 0;
} finally {
    System.out.println("last");
}

答:可以正常运行。

4. 以下代码 catch 里也发生了异常,程序会怎么执行?

try {
    int i = 10 / 0;
    System.out.println("try");
} catch (Exception e) {
    int j = 2 / 0;
    System.out.println("catch");
} finally {
    System.out.println("finally");
}
System.out.println("main");

答:程序会打印出 finally 之后抛出异常并终止运行。

5. 以下代码 finally 里也发生了异常,程序会怎么运行?

try {
    System.out.println("try");
} catch (Exception e) {
    System.out.println("catch");
} finally {
    int k = 3 / 0;
    System.out.println("finally");
}
System.out.println("main");

答:程序在输出 try 之后抛出异常并终止运行,不会再执行 finally 异常之后的代码。

6. 常见的运行时异常都有哪些?

答:常见的运行时异常如下:

  • java.lang.NullPointerException 空指针异常;出现原因:调用了未经初始化的对象或者是不存在的对象;
  • java.lang.ClassNotFoundException 指定的类找不到;出现原因:类的名称和路径加载错误,通常是程序

试图通过字符串来加载某个类时引发的异常;

  • java.lang.NumberFormatException 字符串转换为数字异常;出现原因:字符型数据中包含非数字型字符;
  • java.lang.IndexOutOfBoundsException 数组角标越界异常,常见于操作数组对象时发生;
  • java.lang.ClassCastException 数据类型转换异常;
  • java.lang.NoClassDefFoundException 未找到类定义错误;
  • java.lang.NoSuchMethodException 方法不存在异常;
  • java.lang.IllegalArgumentException 方法传递参数错误。
7. Exception 和 Error 有什么区别?

答:Exception 和 Error 都属于 Throwable 的子类,在 Java 中只有 Throwable
及其之类才能被捕获或抛出,它们的区别如下:

  • Exception(异常)是程序正常运行中,可以预期的意外情况,并且可以使用 try/catch 进行捕获处理的。Exception 又分为运行时异常(Runtime Exception)和受检查的异常(Checked Exception),运行时异常编译能通过,但如果运行过程中出现这类未处理的异常,程序会终止运行;而受检查的异常,要么用 try/catch 捕获,要么用 throws 字句声明抛出,否则编译不会通过。
  • Error(错误)是指突发的非正常情况,通常是不可以恢复的,比如 Java 虚拟机内存溢出,诸如此类的问题叫做 Error。
8. throw 和 throws 的区别是什么?

答:它们的区别如下:

  • throw 语句用在方法体内,表示抛出异常由方法体内的语句处理,执行 throw 一定是抛出了某种异常;
  • throws 语句用在方法声明的后面,该方法的调用者要对异常进行处理,throws 代表可能会出现某种异常,并不一定会发生这种异常。
9. Integer.parseInt(null) 和 Double.parseDouble(null) 抛出的异常一样吗?为什么?

答:Integer.parseInt(null) 和 Double.parseDouble(null) 抛出的异常类型不一样,如下所示:

  • Integer.parseInt(null) 抛出的异常是 NumberFormatException;
  • Double.parseDouble(null) 抛出的异常是 NullPointerException。

至于为什么会产生不同的异常,其实没有特殊的原因,主要是由于这两个功能是不同人开发的,因而就产生了两种不同的异常信息。

10. NoClassDefFoundError 和 ClassNoFoundException 有什么区别?
  • NoClassDefFoundError 是 Error(错误)类型,而 ClassNoFoundExcept 是 Exception(异常)类型;
  • ClassNoFoundExcept 是 Java 使用 Class.forName 方法动态加载类,没有加载到,就会抛出 ClassNoFoundExcept 异常;
  • NoClassDefFoundError 是 Java 虚拟机或者 ClassLoader 尝试加载类的时候却找不到类订阅导致的,也就是说要查找的类在编译的时候是存在的,运行的时候却找不到,这个时候就会出现 NoClassDefFoundError 的错误。
11. 使用 try-catch 为什么比较耗费性能?

答:这个问题要从 JVM(Java 虚拟机)层面找答案了。首先 Java
虚拟机在构造异常实例的时候需要生成该异常的栈轨迹,这个操作会逐一访问当前线程的栈帧,并且记录下各种调试信息,包括栈帧所指向方法的名字,方法所在的类名、文件名,以及在代码中的第几行触发该异常等信息,这就是使用异常捕获耗时的主要原因了。

12. 常见的 OOM 原因有哪些?

答:常见的 OOM 原因有以下几个:

  • 数据库资源没有关闭;
  • 加载特别大的图片;
  • 递归次数过多,并一直操作未释放的变量。
13. 以下程序的返回结果是?

public static int getNumber() {
    try {
        int number = 0 / 1;
        return 2;
    } finally {
        return 3;
    }
}

A:0

B:2

C:3

D:1

答:3

题目解析:程序最后一定会执行 finally 里的代码,会把之前的结果覆盖为 3。

14. finally、finalize 的区别是什么?

答:finally、finalize 的区别如下:

  • finally 是异常处理语句的一部分,表示总是执行;
  • finalize 是 Object 类的一个方法,子类可以覆盖该方法以实现资源清理工作,垃圾回收之前会调用此方法。
15. 为什么 finally 总能被执行?

答:finally 总会被执行,都是编译器的作用,因为编译器在编译 Java 代码时,会复制 finally 代码块的内容,然后分别放在 try-catch
代码块所有的正常执行路径及异常执行路径的出口中,这样 finally 才会不管发生什么情况都会执行。


玩转时间操作 + 面试题

在 JDK 8 之前,Java 语言为我们提供了两个类用于操作时间,它们分别是:java.util.Date 和 java.util.Calendar,但在
JDK 8
的时候为了解决旧时间操作类的一些缺陷,提供了几个新的类,用于操作时间和日期,它们分别是:LocalTime、LocalDateTime、Instant,都位于
java.time 包下。

时间的操作在我们日常的开发中经常见到,比如,业务数据都要记录创建时间和修改时间,并要把这些时间格式化之后显示到前端页面,再比如我们需要计算业务数据的时间间隔等,都离不开对时间的操作,那如何正确而优雅地使用时间?这就是我们接下来要讨论的话题。

时间基础知识科普

格林威治时间

格林威治(又译格林尼治)是英国伦敦南郊原格林威治天文台的所在地,它是世界计算时间和地球经度的起点,国际经度会议 1884
年在美国华盛顿召开,会上通过协议,以经过格林威治天文台的经线为零度经线(即本初子午线),作为地球经度的起点,并以格林威治为“世界时区”的起点。

格林威治时间和北京时间的关系

格林威治时间被定义为世界时间,就是 0 时区,北京是东八区。也就是说格林威治时间的 1 日 0 点,对应到北京的时间就是 1 日 8 点。

时间戳

时间戳是指格林威治时间 1970-01-01 00:00:00(北京时间 1970-01-01 08:00:00)起至现在的总秒数。

JDK 8 之前的时间操作

1 获取时间

Date date = new Date();
System.out.println(date);
Calendar calendar = Calendar.getInstance();
Date time = calendar.getTime();
System.out.println(time);
2 获取时间戳

long ts = new Date().getTime();
System.out.println(ts);
long ts2 = System.currentTimeMillis();
System.out.println(ts2);
long ts3 = Calendar.getInstance().getTimeInMillis();
System.out.println(ts3);
3 格式化时间

SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(sf.format(new Date()));  // output:2019-08-16 21:46:22

SimpleDateFormat 构造参数的含义,请参考以下表格信息:

字符含义示例
yyyyy-1996
MMM-07
d月中的天数dd-02
D年中的天数121
E星期几星期四
H小时数(0-23)HH-23
h小时数(1-12)hh-11
m分钟数mm-02
s秒数ss-03
Z时区+0800

使用示例:

  • 获取星期几:new SimpleDateFormat(“E”).format(new Date())
  • 获取当前时区:new SimpleDateFormat(“Z”).format(new Date*())

注意事项 :在多线程下 SimpleDateFormat 是非线程安全的,因此在使用 SimpleDateFormat
时要注意这个问题。在多线程下,如果使用不当,可能会造成结果不对或内存泄漏等问题。

4 时间转换

SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// String 转 Date
String str = "2019-10-10 10:10:10";
System.out.println(sf.parse(str));
//时间戳的字符串 转 Date
String tsString = "1556788591462";
// import java.sql
Timestamp ts = new Timestamp(Long.parseLong(tsString)); // 时间戳的字符串转 Date
System.out.println(sf.format(ts));

注意事项 :当使用 SimpleDateFormat.parse() 方法进行时间转换的时候,SimpleDateFormat
的构造函数必须和待转换字符串格式一致。

5 获得昨天此刻时间

Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DATE, -1);
System.out.println(calendar.getTime());

JDK 8 时间操作

JDK 8 对时间操作新增了三个类:LocalDateTime、LocalDate、LocalTime。

  • LocalDate 只包含日期,不包含时间,不可变类,且线程安全。
  • LocalTime 只包含时间,不包含日期,不可变类,且线程安全。
  • LocalDateTime 既包含了时间又包含了日期,不可变类,且线程安全。

线程安全性

值得一提的是 JDK 8 中新增的这三个时间相关的类,都是线程安全的,这极大地降低了多线程下代码开发的风险。

1 获取时间

// 获取日期
LocalDate localDate = LocalDate.now();
System.out.println(localDate);    // output:2019-08-16
// 获取时间
LocalTime localTime = LocalTime.now();
System.out.println(localTime);    // output:21:09:13.708
// 获取日期和时间
LocalDateTime localDateTime = LocalDateTime.now();
System.out.println(localDateTime);    // output:2019-08-16T21:09:13.708
2 获取时间戳

long milli = Instant.now().toEpochMilli(); // 获取当前时间戳(精确到毫秒)
long second = Instant.now().getEpochSecond(); // 获取当前时间戳(精确到秒)
System.out.println(milli);  // output:1565932435792
System.out.println(second); // output:1565932435
3 时间格式化

// 时间格式化①
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String timeFormat = dateTimeFormatter.format(LocalDateTime.now());
System.out.println(timeFormat);  // output:2019-08-16 21:15:43
// 时间格式化②
String timeFormat2 = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
System.out.println(timeFormat2);    // output:2019-08-16 21:17:48
4 时间转换

String timeStr = "2019-10-10 06:06:06";
LocalDateTime dateTime = LocalDateTime.parse(timeStr,DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
System.out.println(dateTime);
5 获得昨天此刻时间

LocalDateTime today = LocalDateTime.now();
LocalDateTime yesterday = today.plusDays(-1);
System.out.println(yesterday);

相关面试题

1. 获取当前时间有几种方式?

答:获取当前时间常见的方式有以下三种:

  • new Date()
  • Calendar.getInstance().getTime()
  • LocalDateTime.now()
2. 如何获取昨天此刻的时间?

答:以下为获取昨天此刻时间的两种方式:

// 获取昨天此刻的时间(JDK 8 以前)
Calendar c = Calendar.getInstance();
c.add(Calendar.DATE,-1);
System.out.println(c.getTime());
// 获取昨天此刻的时间(JDK 8)
LocalDateTime todayTime = LocalDateTime.now();
System.out.println(todayTime.plusDays(-1));
3. 如何获取本月的最后一天?

答:以下为获取本月最后一天的两种方式:

// 获取本月的最后一天(JDK 8 以前)
Calendar ca = Calendar.getInstance();
ca.set(Calendar.DAY_OF_MONTH, ca.getActualMaximum(Calendar.DAY_OF_MONTH));
System.out.println(ca.getTime());
// 获取本月的最后一天(JDK 8)
LocalDate today = LocalDate.now();
System.out.println(today.with(TemporalAdjusters.lastDayOfMonth()));
4. 获取当前时间的时间戳有几种方式?

答:以下为获取当前时间戳的几种方式:

  • System.currentTimeMillis()
  • new Date().getTime()
  • Calendar.getInstance().getTime().getTime()
  • Instant.now().toEpochMilli()
  • LocalDateTime.now().toInstant(ZoneOffset.of(“+8”)).toEpochMilli()

其中,第四种和第五种方式是 JDK 8 才新加的。

5. 如何优雅地计算两个时间的相隔时间?

答:JDK 8 中可以使用 Duration 类来优雅地计算两个时间的相隔时间,代码如下:

LocalDateTime dt1 = LocalDateTime.now();
LocalDateTime dt2 = dt1.plusSeconds(60);
Duration duration = Duration.between(dt1, dt2);
System.out.println(duration.getSeconds());  // output:60
6. 如何优雅地计算两个日期的相隔日期?

答:JDK 8 中可以使用 Period 类来优雅地计算两个日期的相隔日期,代码如下:

LocalDate d1 = LocalDate.now();
LocalDate d2 = d1.plusDays(2);
Period period = Period.between(d1, d2);
System.out.println(period.getDays());   //output:2
7. SimpleDateFormat 是线程安全的吗?为什么?

答:SimpleDateFormat 是非线程安全的。因为查看 SimpleDateFormat
的源码可以得知,所有的格式化和解析,都需要通过一个中间对象进行转换,这个中间对象就是
Calendar,这样的话就造成非线程安全。试想一下当我们有多个线程操作同一个 Calendar
的时候后来的线程会覆盖先来线程的数据,那最后其实返回的是后来线程的数据,因此 SimpleDateFormat 就成为了非线程的了。

8. 怎么保证 SimpleDateFormat 的线程安全?

答:保证 SimpleDateFormat 线程安全的方式如下:

  • 使用 Synchronized,在需要时间格式化的操作使用 Synchronized 关键字进行包装,保证线程堵塞格式化;
  • 手动加锁,把需要格式化时间的代码,写到加锁部分,相对 Synchronized 来说,编码效率更低,性能略好,代码风险较大(风险在于不要忘记在操作的最后,手动释放锁);
  • 使用 JDK 8 的 DateTimeFormatter 替代 SimpleDateFormat。
9. JDK 8 中新增的时间类都有哪些优点?

答:JDK 8 中的优点具体有以下几个优点,如下:

  • 线程安全性
  • 使用的便利性(如获取当前时间戳的便利性、增减日期的便利性等)
  • 编写代码更简单优雅,如当前时间的格式化:LocalDateTime.now().format(DateTimeFormatter.ofPattern(“yyyy-MM-dd HH:mm:ss”));
10. 如何比较两个时间(Date)的大小?

答:时间比较有以下三种方式:

  • 获取两个时间的时间戳,得到两个 long 类型的变量,两个变量相减,通过结果的正负值来判断大小;
  • 通过 Date 自带的 before()、after()、equals() 等方法比较,代码示例 date1.before(date2);
  • 通过 compareTo() 方法比较,代码示例:date1.compareTo(date2),返回值 -1 表示前一个时间比后一个时间小,0 表示两个时间相等,1 表示前一个时间大于后一个时间。

总结

JDK 8 之前使用 java.util.Date 和 java.util.Calendar
来操作时间,它们有两个很明显的缺点,第一,非线程安全;第二,API 调用不方便。JDK 8 新增了几个时间操作类 java.time 包下的
LocalDateTime、LocalDate、LocalTime、Duration(计算相隔时间)、Period(计算相隔日期)和
DateTimeFormatter,提供了多线程下的线程安全和易用性,让我们可以更好的操作时间。

数组和排序算法的应用 + 面试题

数组的定义与使用

数组是 Java 编程中最重要的数据结构之一,也是最基本的数据结构,Java 中的常用集合 ArrayList、HashMap
等内部的实现都使用到了数组结构。数组是只能用来存储一种类型的集合,可以通过下标访问数值中的所有元素。

数组的声明方式有以下两种,如整数型数组,请参考下面代码:

  • 方式一:int[] arr;
  • 方式二:int arr[];

大部分情况下,我们会使用第一种方式 int[] arr; 来声明数组。

数组初始化

数组可使用 new int[n] 进行初始化,每个元素初始化为 0,声明了 n 个元素。也可以直接赋值,例如 new int[]{ 1,2,3…
},具体用法可参照下面代码:

// 初始化方式一
int[] arr = new int[5];
// 初始化方式二
int[] arr2 = new int[]{1, 2, 3, 4, 5};
// 初始化方式二的延伸版,可省略 new int[] 直接赋值
int[] arr3 = {1, 2, 3, 4, 5};

注意 :在 Java 中,数组初始化如果声明了数组长度,则不能直接赋值。例如,int[] arr = new int[5]{1, 2, 3,
4, 5}; 给这段初始化数组长度并赋值时,编译器会报错,编译不通过。

数组遍历

数组遍历的常见方式有三种:传统的 for 循环、for each 遍历、还有 JDK 8 中新增的 Lambda 表达式。具体的实现请参考以下实例。

方式一:传统 for 循环

Integer[] arr = {2, 3, 6, 7, 9};
// 方式一:传统 for
for (int i = 0; i < arr.length; i++) {
  System.out.println(arr[i]);
}

方式二:for each

Integer[] arr = {2, 3, 6, 7, 9};
// 方式二:for each
for (int i : arr) {
  System.out.println(i);
}

方式三:JDK 8 中的 Lambda 表达式

Integer[] arr = {2, 3, 6, 7, 9};
// 方式三:jdk 8 Lambda
Arrays.asList(arr).forEach(x -> System.out.println(x));

其中 for each 的方式,写法更简洁,也更不容易出错,不必为数组的越界而担心(大于元素的最大下标值)。

注意 :数组的访问是从 0 开始,而不是 1 开始,也就是第一个元素的获取是 arr[0],而非 arr[1]。

数组拷贝

数组拷贝使用的是 Arrays.copyof() 方法,具体实现请参考下面代码:

int[] arr = {3, 4, 9};
int[] arr2 = Arrays.copyOf(arr, arr.length);
System.out.println(Arrays.toString(arr2));

程序执行结果:[3, 4, 9]

注意 :Arrays.copyOf(array,newLength) 第二个参数 newLength
表示声明此数组的长度,可以比拷贝的数组的长度长,多出来的元素会初始化为 0 值。

数组填充与合并

数组填充

即为每个元素统一赋值,使用 Arrays.fill() 进行数组填充,具体实现请参考下面代码:

int[] arr = new int[10];
Arrays.fill(arr, 6);
System.out.println(Arrays.toString(arr));

程序执行结果:[6, 6, 6, 6, 6, 6, 6, 6, 6, 6]

注意 :使用 Arrays.fill() 会覆盖原有的值,即使数组之前有赋值操作,也会被覆盖。

数组合并

使用 org.apache.commons.lang3.ArrayUtils.addAll() 方法,具体实现请参考下面代码:

int[] arr = {2, 8, 13, 11, 6, 7};
int[] arr2 = {66, 88};
// 合并数组
int[] arr3 = org.apache.commons.lang3.ArrayUtils.addAll(arr, arr2);
System.out.println(Arrays.toString(arr3));

程序执行结果:[2, 8, 13, 11, 6, 7, 66, 88]

排序与算法

数组排序

使用 Arrays.sort() 方法,具体实现请参考下面代码:

int[] arr = {2, 8, 13, 11, 6, 7};
Arrays.sort(arr);
System.out.println(Arrays.toString(arr));

程序执行结果:[2, 6, 7, 8, 11, 13]

数组逆序

使用 org.apache.commons.lang3.ArrayUtils.reverse(arr) 方法,具体实现请参考下面代码:

int[] arr = {2, 8, 13, 11, 6, 7};
int[] arr = {2, 8, 13, 11, 6, 7};
// 数组正序(排序)
Arrays.sort(arr);
// 数组逆序
org.apache.commons.lang3.ArrayUtils.reverse(arr);
System.out.println(Arrays.toString(arr));

程序执行结果:[13, 11, 8, 7, 6, 2]

注意 :org.apache.commons.lang3.ArrayUtils.reverse() 是数组逆序,并不是数组倒序,也就是说
ArrayUtils.reverse() 只会把数组原顺序颠倒输出,并不会自然排序后再倒序输出。

冒泡排序

依次比较相邻的两个数,把较大的值放后面,执行整个循环之后,数组就从小到大进行排列了。具体实现请参考下面代码:

int[] arr = {2, 8, 13, 11, 6, 7};
System.out.println("排序前:" + Arrays.toString(arr));
for (int i = 0; i < arr.length; i++) {
    // 因为冒泡是把每轮循环中较大的数飘到后面,所以是 arr.length-i-1
    for (int j = 0; j < arr.length - i - 1; j++) {
        if (arr[j] > arr[j + 1]) {
            // 元素交换
            int temp = arr[j + 1];
            arr[j + 1] = arr[j];
            arr[j] = temp;
        }
    }
}
System.out.println("排序后:" + Arrays.toString(arr));

程序执行结果:

排序前:[2, 8, 13, 11, 6, 7]
排序后:[2, 6, 7, 8, 11, 13]
选择排序

每次从待排序的数据元素中选出最小(或最大)的一个元素,顺序放在已排好序的数列的最后,直到全部待排序的数据元素排完。具体实现请参考下面代码:

int[] arr = {2, 8, 13, 11, 6, 7};
System.out.println("排序前:" + Arrays.toString(arr));
for (int i = 0; i < arr.length; i++) {
  int lowerIndex = i;
  for (int j = i + 1; j < arr.length; j++) {
    // 找出最小的一个索引
    if (arr[j] < arr[lowerIndex]) {
      lowerIndex = j;
    }
  }
  // 交换
  int temp = arr[i];
  arr[i] = arr[lowerIndex];
  arr[lowerIndex] = temp;
}
System.out.println("排序后:" + Arrays.toString(arr));

程序执行结果:

排序前:[2, 8, 13, 11, 6, 7]
排序后:[2, 6, 7, 8, 11, 13]

关于更多的排序算法,后面会有专门的章节进行介绍。

元素查找

查找数组是否包含某个值,使用 Arrays.binarySearch() 方法查询。 Arrays.binarySearch()
是利用二分法查询某个值,如果查到包含某值会返回该值的下标,如果没有查到则返回负值。

int[] arr = {1, 3, 4, 5};
// Arrays.binarySearch() 使用二分法查询某值
int index = Arrays.binarySearch(arr, 5);
System.out.println(index);

注意 :使用 Arrays.binarySearch 之前一定要先调用 Arrays.sort() 对数组进行排序,否则返回的结果有误。

多维数组

我们之前使用的数组可以称之为一维数组,而多维数组可以理解为数组的数组,可以用二维数组来举例,二维数组也是一种特殊的多维数组。

比如我们声明一个二维数组:int[][] arr = new int[2][4];

这就相当于我们创建了一个两行四列的表,它的使用、赋值与取值,请查看下面代码示例:

// 声明二维数组
int[][] arr = new int[2][4];
//循环二维数组
for (int i = 0; i < arr.length; i++) {
    for (int j = 0; j < arr[0].length; j++) {
        // 二维数组赋值
        arr[i][j] = j;
    }
}
// 二维数组取值
System.out.println(arr[0][1]);
// 打印二维数组
System.out.println(Arrays.toString(arr[0]));
System.out.println(Arrays.toString(arr[1]));

以上程序执行的结果是:

1
[0, 1, 2, 3]
[0, 1, 2, 3]

数组类型转换

字符串转数组

使用 split 分隔字符串就形成了数组,请参考以下代码:

String str = "laowang,stone,wanglei";
String[] arr = str.split(","); // 字符串转数组
System.out.println(arr[0]);
数组转字符串

使用 Arrays.toString() 方法,请参考以下代码:

String[] arr = {"laowang", "stone", "wanglei"};
String str = Arrays.toString(arr);
System.out.println(str);

若要查看更多数组转字符串的方式,请查看本文面试部分的介绍。

数组转集合

使用 Arrays.asList() 方法,请参考以下代码:

String[] strArr = {"cat", "dog"};
List list = Arrays.asList(strArr);
System.out.println(list);
集合转数组

使用 List.toArrray() 方法,请参考以下代码:

List<String> list = new ArrayList<String>();
list.add("cat");
list.add("dog");
// 集合转换为数组
String[] arr = list.toArray(new String[list.size()]);
System.out.println(Arrays.toString(arr));

相关面试题

1. 数组和集合有什么区别?

答:数组和集合的区别如下:

  • 集合可以存储任意类型的对象数据,数组只能存储同一种数据类型的数据;
  • 集合的长度是会发生变化的,数组的长度是固定的;
  • 集合相比数组功能更强大,数组相比集合效率更高。
2. 以下代码访问数组元素打印的结果是多少?

int[] arr = new int[5] {1, 2, 3, 4, 5};
System.out.println(arr[4]);

答:程序编译报错,在 Java 中初始化数组时,如果直接给数组赋值,不能声明数组长度;如果声明了数组长度,则不能赋值给数组,否则编译器报错。

正确的写法如下:

int[] arr = new int[]{1, 2, 3, 4, 5};
System.out.println(arr[4]);

输出的结果为:5,访问元素从 0 开始。

3. 执行以下代码会输出什么结果?

public static void main(String[] args) {
    int[] arr = {2, 3, 4, 8};
    change(arr);
    System.out.println(arr[2]);
}
private static void change(int[] arr) {
    for (int i = 0; i < arr.length; i++) {
        if (i % 2 == 0) {
            arr[i] *= i;
        }
    }
}

答:输出的结果是 8。

题目解析:在 Java 中数组本质是引用类型,因此在调用方法中修改数组,就是对原数组本身的修改。

4. 以下程序打印的结果是多少?

int[] intArr = new int[3];
String[] StrArr = new String[3];
System.out.println(intArr[1]);
System.out.println(StrArr[1]);

答:以上程序打印的结果是:0 和 null。

题目解析:new int[3] 相当于声明了数组的长度为 3,每个元素初始化为 0,而 new String[3] 相当于声明了数组的长度为
3,每个元素初始化为 null。

5. 数组转换字符串有哪些方式?

答:数组转换字符串,有以下几种方式。

方式一:遍历拼接,完整代码如下:

String[] arr = {"laowang", "stone", "wanglei"};
StringBuffer sb = new StringBuffer();
for (int i = 0; i < arr.length; i++) {
    sb.append(arr[i]);
    if (i != arr.length - 1)
        sb.append(",");
}
System.out.println(sb.toString());

方式二:Arrays.toString() 转换,完整代码如下:

String[] arr = {"laowang", "stone", "wanglei"};
String str2 = Arrays.toString(arr);
System.out.println(str2);

方式三:StringUtils.join() 转换,完整代码如下:

String[] arr = {"laowang", "stone", "wanglei"};
String str3 = StringUtils.join(Arrays.asList(arr), ","); // 使用英文逗号分隔
System.out.println(str3);
6. 数组遍历有哪几种方式?

答:常见的数组遍历有以下三种方式。

  • 传统 for 循环,如 for (int i = 0; i < arr.length; i++) { //… }
  • for each 循环,如 for (int i : arr) { //… }
  • jdk 8 Lambda 方式,如 Integer[] arr = {2, 3, 6, 7, 9}; Arrays._asList_(arr).forEach(x -> System._out_.println(x));
7. 以下数组比较的结果分别是什么?

String[] strArr = {"dog", "cat", "pig", "bird"};
String[] strArr2 = {"dog", "cat", "pig", "bird"};
System.out.println(Arrays.equals(strArr, strArr2));
System.out.println(strArr.equals(strArr2));
System.out.println(strArr == strArr2);

答:上面代码执行的结果,分别为:true、false、false。

题目解析:strArr == strArr2 为引用比较,因此结果一定是 false,而数组本身的比较也就是 strArr.equals(strArr2)
为 false 的原因是因为数组没有重写 equals 方法,因此也是引用比较。数组 equals 源码实现如下:

public boolean equals(Object obj) {
  return (this == obj);
}

而 Arrays.equals 的结果之所以是 true 是因为 Arrays.equals 重写了 equals 方法。源代码实现如下:

public static boolean equals(Object[] a, Object[] a2) {
        if (a==a2)
            return true;
        if (a==null || a2==null)
            return false;
        int length = a.length;
        if (a2.length != length)
            return false;
        for (int i=0; i<length; i++) {
            Object o1 = a[i];
            Object o2 = a2[i];
            if (!(o1==null ? o2==null : o1.equals(o2)))
                return false;
        }
        return true;
    }
8. 以下程序使用 Arrays.binarySearch 返回的结果是 true 还是 false?

String[] arr = {"dog", "cat", "pig", "bird"};
int result = Arrays.binarySearch(arr, "bird");
System.out.println(result == -1);

答:返回的结果是:true。

题目解析:使用 Arrays.binarySearch 之前一定要先调用 Arrays.sort() 对数组进行排序,否则返回的结果有误,本数组返回的结果是
﹣1,是因为没有使用排序的结果,正确的使用请查看以下代码:

String[] arr = {"dog", "cat", "pig", "bird"};
Arrays.sort(arr);
int result = Arrays.binarySearch(arr, "bird");
System.out.println(result == -1);
9. Arrays 对象有哪些常用的方法?

答:Arrays 常用方法如下:

  • Arrays.copyOf() 数组拷贝
  • Arrays.asList() 数组转为 List 集合
  • Arrays.fill() 数组赋值
  • Arrays.sort() 数组排序
  • Arrays.toString() 数组转字符串
  • Arrays.binarySearch() 二分法查询元素
  • Arrays.equals() 比较两个数组的值
10. 查询字符串数组中是否包含某个值有几种方法?

答:常见查询数组中是否包含某个值有以下两种方式:

  • 方式一:Arrays.asList(array).contains(“key”);
  • 方式二:Arrays.binarySearch(array, “key”);

具体的实现代码如下:

String[] arr = {"doc", "pig", "cat"};
// 方式一:Arrays.asList(array).contains
boolean bool = Arrays.asList(arr).contains("cat");
System.out.println(bool);
// 方式二:Arrays.binarySearch
Arrays.sort(arr);
boolean bool2 = Arrays.binarySearch(arr, "cat") > -1;
System.out.println(bool2);
11. 如何修改数组的第三个到第五个元素的值为 6?

答:本题考察的知识点显然不是使用 for 循环修改那么简单,而是考察对 Arrays.fill() 方法的掌握,以下提供了两种实现方式可供参考。

方式一:for 循环方式

int[] arrInt = new int[10];
for (int i = 0; i < arrInt.length; i++) {
    if (i >= 2 && i < 5) {
        arrInt[i] = 6;
    }
}

方式二:Arrays.fill() 方式

int[] arrInt = new int[10];
Arrays.fill(arrInt, 2, 5, 6);

总结

在 Java 中数组本质是引用类型,数组只能用来存储固定大小的同类型元素。在 Java 中很多集合的内部都是依赖数组实现的,如 ArrayList 和
HashMap
等。数组的冒泡排序和选择排序也是面试常考的内容,很多公司会要求面试者手写冒泡排序。本文也介绍了数组、字符串和集合之间的相互转换,只有掌握好这些技能才能开发出更好的
Java 程序。

类与 Object 的应用 + 面试题

类介绍

Java 程序是由若干个类组成的,类也是面向对象编程思想的具体实现。

以下为类的基本使用:

public class Cat {
    // 私有属性
    private String name;
    private int age;
    // 构造方法
    public Cat() {
    }
    // 普通方法
    public void eat() {
        System.out.println("吃吃吃");
    }
    // 对外包装属性
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
}
类引用

当我们需要使用不同包下的类时,就需要使用 import 导入包或类,这个时候才能正常使用。例如,我们要使用 java.util 下的 ArrayList
就必须使用 import java.util.ArrayList,请参考以下代码:

// 导入 ArrayList 类
import java.util.ArrayList;
class importTest {
    public static void main(String[] args) {
        ArrayList list = new ArrayList();
    }
}

类引用的高级用法

import 还可以导入静态方法和静态域的功能,比如以下代码:

// 导入 static 静态域的功能
import static java.lang.System.*;
class staticTest {
    public static void main(String[] args) {
        out.println("hi");
    }
}

以上代码也可以顺利的执行,这也是 import 好玩的一个地方。

访问修饰符

在 Java 中访问修饰符有以下四种:

  • public
  • protected
  • 默认
  • private

具体介绍如下表:

访问级别访问控制修饰符同类同包子类不同的包
公开public
受保护protected×
默认没有访问修饰符××
私有private×××

(1)在开发中要尽可能地加上访问修饰符(提高程序的可读性);

(2)无特殊要求的情况下,类内部的变量应该设置为私有的(防止外部篡改)。

构造方法

构造方法也叫构造器或构造函数,它的作用是对类进行初始化,比如以下代码:

class Cat {
    // 构造方法
    public Cat(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public static void main(String[] args) {
        Cat cat = new Cat("喵星人",2);
        System.out.println(cat.getName());
        System.out.println(cat.getAge());
    }
    private String name;
    private int age;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
}

以上代码执行结果如下:

喵星人
2

构造方法五大原则:

1. 构造方法必须与类同名;
2. 构造方法的参数可以没有或者有多个;
3. 构造方法不能有返回值;
4. 每个类可以有一个或多个构造方法;
5. 构造方法总是伴随着 new 操作一起使用。
继承

用法:使用 extends 关键字来实现类的继承,示例代码如下:

class Animal {
    public void eat() {
        System.out.println("Animal");
    }
}
class Cat extends Animal {
}
public class eTest implements Cloneable {
    public static void main(String[] args) {
        Animal cat = new Cat();
        cat.eat();
    }
}

以上程序执行结果:Animal

继承使用技巧:

  • 将公共的变量或者方法提取到超类中;
  • 除非所有的方法都有继承的意义,否则不要使用继承;
  • 在方法覆盖时不要改变原有方法的预期行为。

Object

Object 类是 Java 中的一个特殊类,它是所有类的父类,Java 中的类都直接或间接的继承自 Object 类。

Object 类的常用方法如下:

  • equals():对比两个对象是否相同
  • getClass():返回一个对象的运行时类
  • hashCode():返回该对象的哈希码值
  • toString():返回该对象的字符串描述
  • wait():使当前的线程等待
  • notify():唤醒在此对象监视器上等待的单个线程
  • notifyAll():唤醒在此对象监视器上等待的所有线程
  • clone():克隆一个新对象

关于更多 Object 的内容,如克隆(深克隆、浅克隆)、线程等待和唤醒,会在后面的章节中详细介绍。

相关面试题

1. 类的组成部分有哪些?

答:在 Java 语言中,类主要是由方法和变量两部分组成。

2. 类与对象有哪些区别?

答:类是一个抽象的概念,是对某一事物的描述;而对象是类的实例,是实实在在存在的个体。比如,“人”就是一个类(一个概念),而老王(王磊)就是实实在在的一个“对象”。

3. Java 中可以多继承吗?

答:Java 中只能单继承,但可以实现多接口。

4. Java 中为什么不能实现多继承?

答:从技术的实现角度来说,是为了降低编程的复杂性。假设 A 类中有一个 m() 方法,B 类中也有一个 m() 方法,如果 C 类同时继承 A 类和 B
类,那调用 C 类的 m() 方法时就会产生歧义,这无疑增加了程序开发的复杂性,为了避免这种问题的产生,Java 语言规定不能多继承类,但可以实现多接口。

5. 覆盖和重载有哪些区别?

答:覆盖和重载的区别如下:

  • 覆盖(Override)是指子类对父类方法的一种重写,只能比父类抛出更少的异常,访问权限不能比父类的小,被覆盖的方法不能是 private,否则只是在子类中重新定义了一个方法;
  • 重载(Overload)表示同一个类中可以有多个名称相同的方法,但这些方法的参数列表各不相同。
6. 以下不属于重载特性的是?

A:方法的参数类型不同
B:方法的返回值不同
C:方法的参数个数不同
D:方法的参数顺序不同

答:B

7. 为什么方法不能根据返回类型来区分重载?

答:因为在方法调用时,如果不指定类型信息,编译器就不知道你要调用哪个方法了。比如,以下代码:

float max(int x,int y);
int max(int x,int y);
// 方法调用
max(1,2);

因为 max(1,2) 没有指定返回值,编译器就不知道要调用哪个方法了。

8. 构造方法有哪些特征?

答:构造方法的特征如下:

  • 构造方法必须与类名相同;
  • 构造方法没有返回类型(void 也不能有);
  • 构造方法不能被继承、覆盖、直接调用;
  • 类定义时提供了默认的无参构造方法;
  • 构造方法可以私有,外部无法使用私有构造方法创建对象。
9. 构造函数能不能被覆盖?能不能被重载?

答:构造函数可以重载,但不能覆盖。

10. 以下说法正确的是?

A:类中的构造方法不能忽略
B:构造方法可以作为普通方法被调用
C:构造方法在对象被 new 时被调用
D:一个类只能有一个构造方法

答:C

11. 以下程序执行的结果是?

class ExecTest {
    public static void main(String[] args) {
        Son son = new Son();
    }
}
class Parent{
    {
        System.out.print("1");
    }
    static{
        System.out.print("2");
    }
    public Parent(){
        System.out.print("3");
    }
}
class Son extends Parent{
    {
        System.out.print("4");
    }
    static{
        System.out.print("5");
    }
    public Son(){
        System.out.print("6");
    }
}

答:打印的结果是:251346

加载顺序如下:

  • 执行父类的静态成员;
  • 执行子类的静态成员;
  • 父类的实例成员和实例初始化;
  • 执行父类构造方法;
  • 子类的实例成员和实例初始化;
  • 子类构造方法。
12. 以下程序执行的结果是?

class A {
    public int x = 0;
    public static int y = 0;
    public void m() {
        System.out.print("A");
    }
}
class B extends A {
    public int x = 1;
    public static int y = 2;
    public void m() {
        System.out.print("B");
    }
    public static void main(String[] args) {
        A myClass = new B();
        System.out.print(myClass.x);
        System.out.print(myClass.y);
        myClass.m();
    }
}

答:打印的结果是:00B

题目解析:在 Java 语言中,变量不能被重写。

13. 以下程序执行的结果是?

class A {
    public void m(A a) {
        System.out.println("AA");
    }
    public void m(D d) {
        System.out.println("AD");
    }
}
class B extends A {
    @Override
    public void m(A a) {
        System.out.println("BA");
    }
    public void m(B b) {
        System.out.println("BD");
    }
    public static void main(String[] args) {
        A a = new B();
        B b = new B();
        C c = new C();
        D d = new D();
        a.m(a);
        a.m(b);
        a.m(c);
        a.m(d);
    }
}
class C extends B{}
class D extends B{}

答:打印结果如下。

BA
BA
BA
AD

题目解析:

  • 第一个 BA:因为 A 的 m() 方法,被子类 B 重写了,所以输出是:BA;
  • 第二个 BA:因为 B 是 A 的子类,当调用父类 m() 方法时,发现 m() 方法被 B 类重写了,所以会调用 B 中的 m() 方法,输出就是:BA;
  • 第三个 BA:因为 C 是 B 的子类,会直接调用 B 的 m() 方法,所以输出就是:BA;
  • 第四个 AD:因为 D 是 A 的子类,所以会调用 A 的 m() 方法,所以输出就是:AD。
14. Java 中的 this 和 super 有哪些区别?

答:this 和 super 都是 Java 中的关键字,起指代作用,在构造方法中必须出现在第一行,它们的区别如下。

  • 基础概念:this 是访问本类实例属性或方法;super 是子类访问父类中的属性或方法。
  • 查找范围:this 先查本类,没有的话再查父类;super 直接访问父类。
  • 使用:this 单独使用时,表示当前对象;super 在子类覆盖父类方法时,访问父类同名方法。
15. 在静态方法中可以使用 this 或 super 吗?为什么?

答:在静态方法中不能使用 this 或 super,因为 this 和 super
指代的都是需要被创建出来的对象,而静态方法在类加载的时候就已经创建了,所以没办法在静态方法中使用 this 或 super。

16. 静态方法的使用需要注意哪些问题?

答:静态方法的使用需要注意以下两个问题:

  • 静态方法中不能使用实例成员变量和实例方法;
  • 静态方法中不能使用 this 和 super。
17. final 修饰符的作用有哪些?

答:final 修饰符作用如下:

  • 被 final 修饰的类不能被继承;
  • 被 final 修饰的方法不能被重写;
  • 被 final 修饰的变量不能被修改。
18. 覆盖 equals() 方法的时候需要遵守哪些规则?

答:Oracle 官方的文档对于 equals() 重写制定的规则如下。

  • 自反性:对于任意非空的引用值 x,x.equals(x) 返回值为真。
  • 对称性:对于任意非空的引用值 x 和 y,x.equals(y) 必须和 y.equals(x) 返回相同的结果。
  • 传递性:对于任意的非空引用值 x、y 和 z,如果 x.equals(y) 返回值为真,y.equals(z) 返回值也为真,那么 x.equals(z) 也必须返回值为真。
  • 一致性:对于任意非空的引用值 x 和 y,无论调用 x.equals(y) 多少次,都要返回相同的结果。在比较的过程中,对象中的数据不能被修改。
  • 对于任意的非空引用值 x,x.equals(null) 必须返回假。

此题目不要求记忆,能知道大概即可,属于加分项题目。

19. 在 Object 中 notify() 和 notifyAll() 方法有什么区别?

答:notify() 方法随机唤醒一个等待的线程,而 notifyAll() 方法将唤醒所有在等待的线程。

20. 如何使用 clone() 方法?

答:如果是同一个类中使用的话,只需要实现 Cloneable 接口,定义或者处理 CloneNotSupportedException
异常即可,请参考以下代码:

class CloneTest implements Cloneable {
    int num;
    public static void main(String[] args) throws CloneNotSupportedException {
        CloneTest ct = new CloneTest();
        ct.num = 666;
        System.out.println(ct.num);
        CloneTest ct2 = (CloneTest) ct.clone();
        System.out.println(ct2.num);
    }
}

如果非内部类调用 clone() 的话,需要重写 clone() 方法,请参考以下代码:

class CloneTest implements Cloneable {
    int num;
    public static void main(String[] args) throws CloneNotSupportedException {
        CloneTest ct = new CloneTest();
        ct.num = 666;
        System.out.println(ct.num);
        CloneTest ct2 = (CloneTest) ct.clone();
        System.out.println(ct2.num);
    }
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
public class CloneTest2 {
    public static void main(String[] args) throws CloneNotSupportedException {
        CloneTest ct = new CloneTest();
        ct.num = 666;
        System.out.println(ct.num);
        CloneTest ct2 = (CloneTest) ct.clone();
        System.out.println(ct2.num);
    }
}

总结

本文我们学习了类的基础用法,类引用:import 和 import
static,访问修饰符的作用,构造函数和继承的特点以及使用技巧等,通过这些内容让我们对整个 Java 程序的组成,有了更加清晰直观的印象。

各种内部类和枚举类的使用 + 面试题

内部类不仅经常出现在各种面试题中,还会在 Java 源码中频频出现,因此只有搞明白了 Java 内部类,才能搞定面试和看懂各种 Java 源码。

内部类

Java 内部类,分为以下四种:

  • 成员内部类
  • 静态成员内部类
  • 局部内部类
  • 匿名内部类

下面分别来看这些内部类的使用。

成员内部类
定义

在一个类中定义了另一个类,则将定义在类中的那个类称之为成员内部类。成员内部类也是最普通的内部类。

使用

成员内部类的使用示例如下:

class Outer {
    public Outer() {
        System.out.println("Outer Class.");
    }
    class Inner {
        public void sayHi() {
            System.out.println("Hi, Inner.");
        }
    }
}

其中 Inner 类则为成员内部类。
而成员内部类的创建和使用,请参考以下完整的示例代码:

class InnerTest {
    public static void main(String[] args) {
        Outer out = new Outer();
        // 创建成员内部类
        Outer.Inner inner = out.new Inner();
        inner.sayHi();
    }
}
class Outer {
    public Outer() {
        System.out.println("Outer Class.");
    }
    class Inner {
        public void sayHi() {
            System.out.println("Hi, Inner.");
        }
    }
}
成员内部类的创建

语法:

Outer.Inner inner = new Outer().new Inner();

内部类访问外部类

语法:

Outer.this.xxx

代码示例:

class Outer {
    private String name = "OuterClass";
    public void sayHi() {
        System.out.println("Hi, Outer.");
    }
    class Inner {
        public void sayHi() {
            // 内部类访问外部类
            Outer.this.sayHi();
            System.out.println(Outer.this.name);
            System.out.println("Hi, Inner.");
        }
    }
}
class InnerTest {
    public static void main(String[] args) {
        Outer.Inner inner =  new Outer().new Inner();
        inner.sayHi();
    }
}
外部类访问内部类

语法:

new Inner().xxx

代码示例:

class Outer {
    public void sayHi() {
        System.out.println(new Inner().name);
        System.out.println("Hi, Outer.");
    }
    private class Inner {
        String name = "InnerClass";
        public void sayHi() {
            System.out.println("Hi, Inner.");
        }
    }
}
class InnerTest {
    public static void main(String[] args) {
        new Outer().sayHi();
    }
}
小结
  • 成员内部类可直接访问外部类(使用:外部类.this.xxx);
  • 外部成员类要访问内部类,必须先建立成员内部类对象;
  • 成员内部类可使用任意作用域修饰(public、protected、默认、private);
  • 成员内部类可访问外部类任何作用域修饰的属性和方法;
  • 外部类建立成员内部类对象之后,可以访问任何作用域修饰的内部类属性和方法。
静态成员内部类
定义

在一个类中定义了另一个 static 类,则将定义在类中的那个 static 类称之为静态成员内部类。

静态成员内部类也就是给内部成员类加上 static 修饰符。

使用

静态成员内部类的使用示例如下:

class OuterClass {
    public OuterClass() {
        System.out.println("OuterClass Init.");
    }
    protected static class InnerClass {
        public void sayHi() {
            System.out.println("Hi, InnerClass.");
        }
    }
}
class InnerClassTest {
    public static void main(String[] args) {
        OuterClass.InnerClass innerClass = new OuterClass.InnerClass();
        innerClass.sayHi();
    }
}

与内部成员类的创建方式 new Outer().new Inner() 不同,静态成员内部类可使用 new OuterClass.InnerClass()
的方式进行创建。

注意 :不能从静态成员内部类中访问非静态外部类对象。

局部内部类
定义

一个类定义在另一个类的局部(方法或者任意作用域),这个类就称之为局部内部类。

使用

局部内部类的使用示例如下:

class OutClass {
    public void sayHi() {
        class InnerClass {
            InnerClass(String name) {
                System.out.println("InnerClass:" + name);
            }
        }
        System.out.println(new InnerClass("Three"));
        System.out.println("Hi, OutClass");
    }
}
class OutTest {
    public static void main(String[] args) {
        new OutClass().sayHi();
    }
}
局部内部类特点
  • 局部内部类不能使用任何访问修饰符;
  • 局部类如果在方法中,可以直接使用方法中的变量,不需要通过 OutClass.this.xxx 的方式获得。
匿名内部类
定义

没有名字的内部类就叫做匿名内部类。

使用

匿名内部类的使用示例如下:

interface AnonymityOuter {
    void hi();
}
class AnonymityTest {
    public static void main(String[] args) {
        AnonymityOuter anonymityOuter = new AnonymityOuter() {
            @Override
            public void hi() {
                System.out.println("Hi, AnonymityOuter.");
            }
        };
        anonymityOuter.hi();
    }
}

其中,new AnonymityOuter() 之后的 {…} 大括号包含的部分就为匿名内部类。

匿名内部类特点
  • 匿名内部类必须继承一个父类或者实现一个接口
  • 匿名内部类不能定义任何静态成员和方法
  • 匿名内部类中的方法不能是抽象的

枚举类

枚举类是 JDK 1.5 引入的新特性,使用关键字“enum”声明。枚举功能虽小,却非常实用,大大方便了程序的开发者。

枚举类的使用

请参考以下代码:

enum ColorEnum {
    RED,
    BLUE,
    YELLOW,
    GREEN
}
class EnumTest {
    public static void main(String[] args) {
        ColorEnum color = ColorEnum.GREEN;
        switch (color) {
            case RED:
                System.out.println("Red");
                break;
            case BLUE:
                System.out.println("Blue");
                break;
            case YELLOW:
                System.out.println("Yellow");
                break;
            case GREEN:
                System.out.println("Green");
                break;
            default:
                break;
        }
    }
}
枚举类命名规范

《阿里巴巴 Java 开发手册》对枚举类的命名规范建议,如下图:

enter image description
here

扩展枚举类

我们可以自定义一些枚举类方法,扩展枚举类的使用,请参考以下代码:

enum ColorsEnum {
    RED("红色", 1),
    BLUE("蓝色", 2),
    YELLOW("黄色", 3),
    GREEN("绿色", 4);
    ColorsEnum(String name, int index) {
        this.name = name;
        this.index = index;
    }
    private String name;
    private int index;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getIndex() {
        return index;
    }
    public void setIndex(int index) {
        this.index = index;
    }
}
class EnumTest {
    public static void main(String[] args) {
        System.out.println(ColorsEnum.RED.getName());
        System.out.println(ColorsEnum.RED.getIndex());
    }
}

执行以上代码返回的结果:

红色
1

相关面试题

1.Java 中的内部类有哪些?

答:内部类包含以下 4 种:

  • 静态内部类:static class StaticInnerClass{};
  • 成员内部类:private class InstanceInnerClass{};
  • 局部内部类:定义在方法或者表达式内部;
  • 匿名内部类:(new Thread(){}).start()。
2.以下关于匿名内部类说法错误的是?

A:匿名内部类必须继承一个父类或者实现一个接口
B:匿名内部类中的方法不能是抽象的
C:匿名内部类可以实现接口的部分抽象方法
D:匿名内部类不能定义任何静态成员和方法

答:C
题目解析:匿名内部类规定必须实现接口的所有抽象方法,否则程序会报错,如下图所示。

enter image description
here

3.以下枚举类比较“==”和“equals”结果一致吗?为什么?

class EnumTest {
    public static void main(String[] args) {
        ColorEnum redColor = ColorEnum.RED;
        ColorEnum redColor2 = ColorEnum.RED;
        System.out.println(redColor == redColor2);
        System.out.println(redColor.equals(redColor2));
    }
}
enum ColorEnum {
    RED,
    BLUE
}

答:结果一致,都是 true
题目分析:因为枚举类重写了 equals 方法,equals 方法里直接使用的 == 比较的,而枚举类不能通过 new 进行创建,使用
ColorEnum.RED 得到的对象,其实使用的是对象的引用地址,所以 == 比较的结果一定是 true。equals 被重写的源码如下图:

enter image description
here

4.使用静态内部类的好处有哪些?

答:使用静态内部类的好处如下:
作用域不会扩散到包外;

  • 可以通过“外部类.内部类”的方式直接访问;
  • 内部类可以访问外部类中的所有静态属性和方法。
5.以下代码执行的结果是?

class OuterClass {
    String name = "OuterClass";
    protected static class InnerClass {
        String name = "InnerClass";
        public void sayHi() {
            System.out.println(OuterClass.this.name);
        }
    }
}
class InnerClassTest {
    public static void main(String[] args) {
        OuterClass.InnerClass innerClass = new OuterClass.InnerClass();
        innerClass.sayHi();
    }
}

答:程序报错。
题目解析:在静态成员内部类中不能直接访问非静态外部类,因此程序会报错。

6.成员内部类和局部内部类有什么区别?

答:内部成员类和局部内部类的区别如下。

  • 内部成员类可以使用任意访问修饰符,局部内部类不能使用任何访问修饰符;
  • 局部内部类是声明在外部类的方法或其他作用域范围内的,内部类是直接声明在外部类之中的,与方法和属性平级。
7.为什么要使用内部类?内部类的使用场景有哪些?

答:使用内部类的好处有以下两个。

  • 可以作为多继承的一种实现方式,最早内部类的实现就是平衡 Java 语言中没有多继承的一种方式;
  • 方便将存在一定逻辑关系的类组织在一起,又可以对外界隐藏。
    内部类可以作为多继承的一种实现方式进行使用,因为每个内部类都能独立的继承一个类或接口,所以整个类就可以实现多继承。
8.以下代码执行的结果是?

class Outer {
    public int num = 1;
    class Inner {
        public int num = 2;
        public void show() {
            int num = 3;
            System.out.println(num);
            System.out.println(this.num);
            System.out.println(Outer.this.num);
        }
    }
}
class InnerTest {
    public static void main(String[] args) {
        new Outer().new Inner().show();
    }
}

答:输出内容如下。

3
2
1
9.枚举有哪些应用场景?

答:枚举类的主要应用场景如下:
① 枚举类可作为高级的常量类
示例代码如下:

public enum Color {
    RED("#FF0000", "255,0,0"),
    GREEN("#00FFFF", "0,255,255"),
    YELLOW("#FFFF00", "255,255,0");
    String hex, rgb;
    Color(String hex, String rgb) {
 this.hex = hex;
 this.rgb = rgb;
    }
}

② 枚举类可方便的用于 switch 判断
示例代码如下:

switch(color)
{
case RED:
    System.out.println("红灯停");
    break;
case GREEN:
    System.out.println("绿灯行");
    break;
case YELLOW:
    System.out.println("看情况");
    break;
default:
    System.out.println("灯坏了");
}
10.枚举类在 JVM 中是如何实现的?

答:枚举类在 JVM(Java 虚拟机) 中其实是通过普通的 static final 形式实现的。
题目解析:我们使用 javap 命令来分析枚举类最终编译的结果,查看编译后的结果,就找到了枚举类在 JVM 中的具体实现了。
首先定义一个枚举类,代码如下:

enum DBEnum {
    ORACLE,
    DB2,
    MYSQL,
    SQLSERVER
}

再使用命令 javac DBEnum.java 编译 .class 文件,然后再使用命令 javap DBEnum.class,我们看到最终执行的结果如下:

Compiled from "EnumTest.java"
final class DBEnum extends java.lang.Enum<DBEnum> {
public static final DBEnum ORACLE;
public static final DBEnum DB2;
public static final DBEnum MYSQL;
public static final DBEnum SQLSERVER;
public static DBEnum[] values();
public static DBEnum valueOf(java.lang.String);
static {};
}

由此可以断定,枚举类在 JVM 中的实现也是通过普通的 static final 实现的。

11.枚举类可以被继承吗?

答:不能被继承,因为枚举类编译后的实际代码是 final class 的形式,类被 final 修饰了自然不能被继承。

12.枚举类是否是线程安全的?

答:枚举类是线程安全的,因为枚举类被编译后是 final class 的形式存在的,所以枚举类是线程安全的。

13.枚举是否可以被序列化?

答:枚举是可以被序列化的,Oracle 官方对此给出了说明,内容如下:

Enum constants are serialized differently than ordinary serializable or
externalizable objects. The serialized form of an enum constant consists
solely of its name; field values of the constant are not transmitted. To
serialize an enum constant, ObjectOutputStream writes the string returned by
the constant’s name method. Like other serializable or externalizable objects,
enum constants can function as the targets of back references appearing
subsequently in the serialization stream. The process by which enum constants
are serialized cannot be customized; any class-specific writeObject and
writeReplace methods defined by enum types are ignored during serialization.
Similarly, any serialPersistentFields or serialVersionUID field declarations
are also ignored–all enum types have a fixed serialVersionUID of 0L

原文地址:https://docs.oracle.com/javase/8/docs/api/java/io/ObjectOutputStream.html
大致的意思是说:枚举的序列化和其他普通类的序列化不同,枚举序列化的时候,只是将枚举对象的 name 属性输出到结果中,反序列化的时候则是通过
java.lang.Enum 的 valueOf 方法根据名字查找枚举对象。

总结

通过本文我们系统地学习了 Java
的各种内部类:静态内部类、成员内部类、局部内部类、匿名内部类,知道了它们特点和区别,并学习了枚举类了使用,知道了枚举类在编译之后,其实还是普通的最终类(final
class)。

抽象类和接口的应用 + 面试题

抽象类

定义

在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。简单来说,使用关键字
abstract 修饰的类就叫做抽象类。

抽象类使用

示例代码,如下:

abstract class AbstractAnimal {
    public AbstractAnimal() {
        System.out.println("Init AbstractAnimal.");
    }
    static String name = "AbstractAnimal";
    public abstract void eat();
    public void run() {
        System.out.println("AbstractAnimal Run.");
    }
}
class Animal extends AbstractAnimal {
    public static void main(String[] args) {
        AbstractAnimal animal = new Animal();
        animal.run();
        System.out.println(animal.name);
        animal.eat();
    }
  // 必须重写抽象父类方法
    @Override
    public void eat() {
        System.out.println("Animal Eat.");
    }
}

以上代码执行的结果:

Init AbstractAnimal.
AbstractAnimal Run.
AbstractAnimal
Animal Eat.
抽象方法

使用 abstract 关键字修饰的方法叫做抽象方法,抽象方法仅有声明没有方法体。如下代码:

public abstract void m();
抽象类的特性
  • 抽象类不能被初始化
  • 抽象类可以有构造方法
  • 抽象类的子类如果为普通类,则必须重写抽象类中的所有抽象方法
  • 抽象类中的方法可以是抽象方法或普通方法
  • 一个类中如果包含了一个抽象方法,这个类必须是抽象类
  • 子类中的抽象方法不能与父类中的抽象方法同名
  • 抽象方法不能为 private、static、final 等关键字修饰
  • 抽象类中可以包含普通成员变量,访问类型可以任意指定,也可以使用静态变量(static)

接口

定义

接口(interface)是抽象类的延伸,它允许一个类可以实现多个接口,弥补了抽象类不能多继承的缺陷,接口是对类的描述,使用 interface
关键字来声明。

接口使用

示例代码,如下:

interface IAnimal {
    void run();
}
class AnimalImpl implements IAnimal {
    public static void main(String[] args) {
        IAnimal animal = new AnimalImpl();
        animal.run();
    }
    @Override
    public void run() {
        System.out.println("AnimalImpl Run.");
    }
}
Java 8 中接口的改动

1)接口中增加了 default 方法和 static 方法,可以有方法体
示例代码,如下:

interface IAnimal {
    static void printSex() {
        System.out.println("Male Dog");
    }
    default void printAge() {
        System.out.println("18");
    }
}
class AnimalImpl implements IAnimal {
    public static void main(String[] args) {
        IAnimal.printSex();
        IAnimal animal = new AnimalImpl();
        animal.printAge();
  }
}

注意 :static 方法属于接口方法,可以直接使用;default 属于实例方法,必须先创建实例。

2)接口中的静态变量会被继承
示例代码,如下:

interface IAnimal {
    static String animalName = "Animal Name";
    static void printSex() {
        System.out.println("Male Dog");
    }
}
class AnimalImpl implements IAnimal {
    public static void main(String[] args) {
        System.out.println(animalName);
        IAnimal.printSex();
    }
}

注意 :静态变量会被继承,静态方法不会被继承。

3)新增函数式接口

函数式接口(Function Interface)是一个特殊的接口,使用 @FunctionInterface 注解声明,定义这种接口可以使用
Lambda 表达式直接调用。
示例代码,如下:

@FunctionalInterface
interface IAnimal {
    static String animalName = "Animal Name";
    static void printSex() {
        System.out.println("Male Dog");
    }
    default void printAge() {
        System.out.println("18");
    }
    void sayHi(String name);
}
class FunctionInterfaceTest {
    public static void main(String[] args) {
        IAnimal animal = name -> System.out.println(name);
        animal.sayHi("WangWang");
    }
}

注意 :使用 @FunctionInterface 声明的函数式接口,抽象方法必须有且仅有一个,但可以包含其他非抽象方法。

相关面试题

1.抽象类中能不能包含方法体?

答:抽象类中可以包含方法体。抽象类的构成也可以完全是包含方法体的普通方法,只不过这样并不是抽象类最优的使用方式。

题目解析:包含了方法体的抽象类示例代码如下:

abstract class AbstractAnimal {
    public void run() {
        System.out.println("AbstractAnimal Run.");
    }
}
class Animal extends AbstractAnimal {
    public static void main(String[] args) {
        AbstractAnimal animal = new Animal();
        animal.run();
    }
}

以上代码执行的结果是: AbstractAnimal Run.

2.抽象类能不能被实例化?为什么?

答:抽象类不能被实例化,因为抽象类和接口的设计就是用来规定子类行为特征的,就是让其他类来继承,是多态思想的一种设计体现,所以强制规定抽象类不能被实例化。

3.抽象方法可以被 private 修饰吗?为什么?

答:抽象方法不能使用 private 修饰,因为抽象方法就是要子类继承重写的,如果设置 private
则子类不能重写此抽象方法,这与抽象方法的设计理念相违背,所以不能被 private 修饰。

4.添加以下哪个选项不会引起编译器报错?

abstract class AbstractAnimal {
    static String animalName = "AbstractAnimal";
      // 添加代码处
}

A:protected abstract void eat();
B: void eat();
C:abstract void eat(){};
D:animalName += “Cat”;

答:A

题目解析:选项 B 普通方法必须有方法体;选项 C 抽象方法不能有方法体;选项 D 变量赋值操作必须在方法内。

5.以下关于抽象类和抽象方法说法正确的是?

A:抽象类中的方法必须全部为抽象方法
B: 抽象类中必须包含一个抽象方法
C:抽象类中不能包含普通方法
D:抽象类中的方法可以全部为普通方法(包含方法体)

答:D

题目解析:抽象类中可以没有方法或者全部为普通方法,都是允许的,如下代码所示:

abstract class AbstractAnimal {
    public void run() {
        System.out.println("AbstractAnimal Run.");
    }
}
class Animal extends AbstractAnimal {
    public static void main(String[] args) {
        AbstractAnimal animal = new Animal();
        animal.run();
    }
}

程序执行的结果为:AbstractAnimal Run.

6.接口和普通类有什么关系?

答:在 Java 语言设计中,接口不是类,而是对类的一组需求描述,这些类必须要遵循接口描述的统一格式进行定义。

7.接口能不能有方法体?

答:JDK 8 之前接口不能有方法体,JDK 8 之后新增了 static 方法和 default 方法,可以包含方法体。

8.执行以下代码会输出什么结果?

interface IAnimal {
    static String animalName = "Animal Name";
}
class AnimalImpl implements IAnimal {
    static String animalName = new String("Animal Name");
    public static void main(String[] args) {
        System.out.println(IAnimal.animalName == animalName);
    }
}

答:执行的结果为 false。

题目解析:子类使用 new String… 重新创建了变量 animalName,又因为使用 == 进行内存地址比较,所以结果就是 false。

9.抽象类和接口有什么区别?

答:抽象类和接口的区别,主要分为以下几个部分。

  • 默认方法
  • 抽象类可以有默认方法的实现
  • JDK 8 之前接口不能有默认方法的实现,JDK 8 之后接口可以有默认方法的实现
  • 继承方式
  • 子类使用 extends 关键字来继承抽象类
  • 子类使用 implements 关键字类实现接口
  • 构造器
  • 抽象类可以有构造器
  • 接口不能有构造器
  • 方法访问修饰符
  • 抽象方法可以用 public / protected / default 等修饰符
  • 接口默认是 public 访问修饰符,并且不能使用其他修饰符
  • 多继承
  • 一个子类只能继承一个抽象类
  • 一个子类可以实现多个接口
10.以下抽象方法描述正确的是?

A:抽象方法可以是静态(static)的
B:抽象方法可同时是本地方法(native)
C:抽象方法可以被 synchronized 修饰
D:以上都不是

答:D

题目解析:抽象方法需要被子类重写,而静态方法是无法被重写的,因此抽象方法不能被静态(static)修饰;本地方法是由本地代码实现的方法,而抽象方法没有实现,所以抽象方法不能同时是本地方法;synchronized
和方法的实现细节有关,而抽象方法不涉及实现细节,因此抽象方法不能被 synchronized 修饰。

总结

抽象类和接口都是面向对象编程中多态的具体实现,在 Java 编程思想中占据着重要的地位,同时也是初级面试岗位必问的问题之一,但由于接口在 JDK 8

        IAnimal animal = new AnimalImpl();
        animal.printAge();
  }
}

注意 :static 方法属于接口方法,可以直接使用;default 属于实例方法,必须先创建实例。

2)接口中的静态变量会被继承
示例代码,如下:

interface IAnimal {
    static String animalName = "Animal Name";
    static void printSex() {
        System.out.println("Male Dog");
    }
}
class AnimalImpl implements IAnimal {
    public static void main(String[] args) {
        System.out.println(animalName);
        IAnimal.printSex();
    }
}

注意 :静态变量会被继承,静态方法不会被继承。

3)新增函数式接口

函数式接口(Function Interface)是一个特殊的接口,使用 @FunctionInterface 注解声明,定义这种接口可以使用
Lambda 表达式直接调用。
示例代码,如下:

@FunctionalInterface
interface IAnimal {
    static String animalName = "Animal Name";
    static void printSex() {
        System.out.println("Male Dog");
    }
    default void printAge() {
        System.out.println("18");
    }
    void sayHi(String name);
}
class FunctionInterfaceTest {
    public static void main(String[] args) {
        IAnimal animal = name -> System.out.println(name);
        animal.sayHi("WangWang");
    }
}

注意 :使用 @FunctionInterface 声明的函数式接口,抽象方法必须有且仅有一个,但可以包含其他非抽象方法。

相关面试题

1.抽象类中能不能包含方法体?

答:抽象类中可以包含方法体。抽象类的构成也可以完全是包含方法体的普通方法,只不过这样并不是抽象类最优的使用方式。

题目解析:包含了方法体的抽象类示例代码如下:

abstract class AbstractAnimal {
    public void run() {
        System.out.println("AbstractAnimal Run.");
    }
}
class Animal extends AbstractAnimal {
    public static void main(String[] args) {
        AbstractAnimal animal = new Animal();
        animal.run();
    }
}

以上代码执行的结果是: AbstractAnimal Run.

2.抽象类能不能被实例化?为什么?

答:抽象类不能被实例化,因为抽象类和接口的设计就是用来规定子类行为特征的,就是让其他类来继承,是多态思想的一种设计体现,所以强制规定抽象类不能被实例化。

3.抽象方法可以被 private 修饰吗?为什么?

答:抽象方法不能使用 private 修饰,因为抽象方法就是要子类继承重写的,如果设置 private
则子类不能重写此抽象方法,这与抽象方法的设计理念相违背,所以不能被 private 修饰。

4.添加以下哪个选项不会引起编译器报错?

abstract class AbstractAnimal {
    static String animalName = "AbstractAnimal";
      // 添加代码处
}

A:protected abstract void eat();
B: void eat();
C:abstract void eat(){};
D:animalName += “Cat”;

答:A

题目解析:选项 B 普通方法必须有方法体;选项 C 抽象方法不能有方法体;选项 D 变量赋值操作必须在方法内。

5.以下关于抽象类和抽象方法说法正确的是?

A:抽象类中的方法必须全部为抽象方法
B: 抽象类中必须包含一个抽象方法
C:抽象类中不能包含普通方法
D:抽象类中的方法可以全部为普通方法(包含方法体)

答:D

题目解析:抽象类中可以没有方法或者全部为普通方法,都是允许的,如下代码所示:

abstract class AbstractAnimal {
    public void run() {
        System.out.println("AbstractAnimal Run.");
    }
}
class Animal extends AbstractAnimal {
    public static void main(String[] args) {
        AbstractAnimal animal = new Animal();
        animal.run();
    }
}

程序执行的结果为:AbstractAnimal Run.

6.接口和普通类有什么关系?

答:在 Java 语言设计中,接口不是类,而是对类的一组需求描述,这些类必须要遵循接口描述的统一格式进行定义。

7.接口能不能有方法体?

答:JDK 8 之前接口不能有方法体,JDK 8 之后新增了 static 方法和 default 方法,可以包含方法体。

8.执行以下代码会输出什么结果?

interface IAnimal {
    static String animalName = "Animal Name";
}
class AnimalImpl implements IAnimal {
    static String animalName = new String("Animal Name");
    public static void main(String[] args) {
        System.out.println(IAnimal.animalName == animalName);
    }
}

答:执行的结果为 false。

题目解析:子类使用 new String… 重新创建了变量 animalName,又因为使用 == 进行内存地址比较,所以结果就是 false。

9.抽象类和接口有什么区别?

答:抽象类和接口的区别,主要分为以下几个部分。

  • 默认方法
  • 抽象类可以有默认方法的实现
  • JDK 8 之前接口不能有默认方法的实现,JDK 8 之后接口可以有默认方法的实现
  • 继承方式
  • 子类使用 extends 关键字来继承抽象类
  • 子类使用 implements 关键字类实现接口
  • 构造器
  • 抽象类可以有构造器
  • 接口不能有构造器
  • 方法访问修饰符
  • 抽象方法可以用 public / protected / default 等修饰符
  • 接口默认是 public 访问修饰符,并且不能使用其他修饰符
  • 多继承
  • 一个子类只能继承一个抽象类
  • 一个子类可以实现多个接口
10.以下抽象方法描述正确的是?

A:抽象方法可以是静态(static)的
B:抽象方法可同时是本地方法(native)
C:抽象方法可以被 synchronized 修饰
D:以上都不是

答:D

题目解析:抽象方法需要被子类重写,而静态方法是无法被重写的,因此抽象方法不能被静态(static)修饰;本地方法是由本地代码实现的方法,而抽象方法没有实现,所以抽象方法不能同时是本地方法;synchronized
和方法的实现细节有关,而抽象方法不涉及实现细节,因此抽象方法不能被 synchronized 修饰。

总结

抽象类和接口都是面向对象编程中多态的具体实现,在 Java 编程思想中占据着重要的地位,同时也是初级面试岗位必问的问题之一,但由于接口在 JDK 8
中的改动比较大,因而面试者在网上搜到的绝大数关于接口和抽象类区别的答案也是不准确的,这点需要面试者特别注意一下。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: 您好,以下是Java基础面试题(2022最新版汇总): 1. Java中的四种访问修饰符是什么?它们分别代表什么含义? 答:Java中的四种访问修饰符是public、protected、default和private。它们分别代表公共的、受保护的、默认的和私有的含义。 2. Java中的八种基本数据类型是什么? 答:Java中的八种基本数据类型是byte、short、int、long、float、double、char和boolean。 3. Java中的自动装箱和拆箱是什么? 答:Java中的自动装箱和拆箱是指将基本数据类型自动转换为对应的包装类类型,以及将包装类类型自动转换为对应的基本数据类型。 4. Java中的final关键字有哪些用途? 答:Java中的final关键字有三种用途:用于修饰变量,表示该变量的值不可改变;用于修饰方法,表示该方法不可被重写;用于修饰类,表示该类不可被继承。 5. Java中的抽象类和接口有什么区别? 答:Java中的抽象类和接口都不能被实例化,但是抽象类可以包含非抽象方法,而接口只能包含抽象方法。另外,一个类只能继承一个抽象类,但是可以实现多个接口。 6. Java中的多态是什么? 答:Java中的多态是指同一个方法可以根据不同的对象调用出不同的行为。它实现的方式有两种:重载和重写。 7. Java中的异常处理机制是什么? 答:Java中的异常处理机制是指通过try-catch-finally语句块来捕获和处理程序运行时可能出现的异常情况,保证程序的稳定性和可靠性。 8. Java中的线程是什么?如何创建线程? 答:Java中的线程是指程序执行的最小单位,它可以独立运行,也可以与其他线程共享资源。创建线程的方式有两种:继承Thread类和实现Runnable接口。 9. Java中的集合框架有哪些?它们之间有什么区别? 答:Java中的集合框架包括List、Set、Map和Queue等。它们之间的区别在于:List是有序的、可重复的集合;Set是无序的、不可重复的集合;Map是键值对的集合;Queue是先进先出的队列。 10. Java中的反射是什么?有什么作用? 答:Java中的反射是指在程序运行时动态地获取类的信息,并可以在运行时操作类的属性和方法。它的作用在于:可以实现动态代理、动态生成代码、动态加载类等功能。 ### 回答2: Java是一种广泛使用的编程语言,它的应用覆盖多个领域,包括机器学习、大数据、企业级应用等。而Java开发人员也是当前市场上非常火热的职业之一。为了进入一家Java开发企业或者获取Java开发岗位,需要通过Java基础面试。 下面我将介绍Java基础面试题,并提供一些答案和解释: 1. 什么是Java? Java是一种面向对象的编程语言,被广泛应用于开发各种应用程序,包括Web应用程序、移动应用程序、桌面应用程序等。 2. 什么是面向对象编程? 面向对象编程是一种编程范型,它将程序分解为对象,每个对象都有自己的数据和方法,对象之间可以相互交互。面向对象编程通常具有封装、继承和多态等特性。 3. Java应用程序的基本结构是什么? Java应用程序的基本结构包括类定义,变量定义,方法定义和语句执行。 4. 什么是Java虚拟机? Java虚拟机是Java平台的核心组件之一,它提供了一个运行Java程序的环境。Java虚拟机可以在不同的操作系统上运行Java程序,实现了跨平台运行的目的。 5. 什么是Java语言的基本数据类型? Java语言的基本数据类型包括byte、short、int、long、float、double、boolean和char。 6. 如何声明一个变量? 在Java中,声明一个变量需要指定变量的类型和名称。例如,int count = 10; 7. 什么是包? 包是在Java中管理类和接口的一种方式。包可以将相关的类和接口组织在一起,以便更好地管理代码。 8. 什么是Java中的异常? 异常是在程序执行期间出现的错误,它包含了错误信息、错误类型和错误的上下文。 9. 什么是Java中的多线程? 多线程是Java中一种并发处理的方法,它允许程序同时执行多个任务。Java中的线程可以通过继承Thread类或实现Runnable接口来创建。 10. Java中的继承关系是什么? 继承是Java中一种对象之间的关系,它允许子类继承父类的属性和方法。子类可以通过继承父类来扩展自己的功能。 以上是常见的Java基础面试题,希望能够对Java开发者提供帮助。在面试过程中,还需要我们展示自己的编程能力和实际应用经验。只有掌握了扎实的Java基础、熟悉常见的应用场景、具备实际经验并且具备坚实的编程能力,才能获得Java开发岗位。 ### 回答3: Java作为目前最热门的编程语言之一,在面试中也是一个重点考察的领域。下面对于Java基础面试题进行汇总和解答。 1. Java常见的数据类型有哪些? 答:Java常见的数据类型有基本数据类型和引用数据类型两种。基本数据类型包括byte、short、int、long、float、double、boolean、char;引用数据类型包括类、数组、接口。 2. int和Integer有什么区别? 答:int是Java中的一种基本数据类型,使用时直接声明并赋值即可;而Integer是int的包装类型,是一个对象,需要通过new来进行初始化,也可以使用自动装箱和拆箱进行转换。 3. String和StringBuilder的区别? 答:String是一个不可变的类,一旦创建就不能修改,修改需要重新创建对象;StringBuilder是一个可变的类,可以通过append、insert等方法修改字符串,节省内存和时间。 4. 抽象类和接口的区别? 答:抽象类和接口都是用于描述行为的概念,但是抽象类更偏向于在类层次结构中描述一种抽象的类,它可以拥有抽象方法和具体实现方法,可以有构造函数,但不能被实例化;而接口更偏向于描述一种能力,只能包含抽象方法、常量和默认方法,不能有构造函数,需要实现接口才能使用。 5. 静态和非静态的区别? 答:静态属性或方法属于类,而非静态属性或方法属于实例,也就是说同一个类属性或方法,静态的只有一份,非静态每次实例化时都有自己的一份;静态变量在类的加载时初始化,公用一份内存,一旦被修改,则所有的对象都会共享该值;非静态变量在每次实例化时初始化,属于独立变量。 6. final关键字的作用? 答:final关键字可以修饰类、方法和变量,表示不可修改、不可继承或不可重写。final修饰的变量是一个常量,一旦初始化后就不能修改;final修饰的方法不能被子类重写;final修饰的类不能被继承。 7. try-catch-finally语句的应用? 答:try-catch-finally语句用于捕获程序中的异常,在try块中尝试执行一段可能会出现异常的代码,如果出现异常则会转到catch块中处理异常,最后无论是否出现异常都会执行finally块中的语句。finally块一般用于释放资源或进行清理操作,如关闭数据库、文件等。 8. 在Java中如何实现多线程? 答:Java中实现多线程有两种方式:继承Thread类和实现Runnable接口。继承Thread类需要重写run方法,而实现Runnable接口需要实现run方法。另外,还可以使用线程池、同步锁、信号量、线程等待等方式实现多线程。 9. 快速判断一个字符串是否是数字? 答:可以使用正则表达式或Java自带的API进行判断。正则表达式可以使用\d+匹配数字,Java自带的API可以使用try-catch捕获NumberFormatException异常来判断。 10. Java中的反射是什么? 答:Java中的反射是指程序在运行时可以动态获取和操控类的信息和对象的属性或方法,包括类的名称、属性和方法。可以通过反射来获取类的所有信息,包括私有的成员变量和方法,也可以在运行时创建对象、调用方法等。但是由于反射操作需要消耗一定的资源,因此不适合在频繁执行的代码中使用。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

办公模板库 素材蛙

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

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

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

打赏作者

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

抵扣说明:

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

余额充值