Java篇:大厂Java语法基础高频面试题及参考答案
大厂在校招面试时,很重视面试者基础是否扎实,如果基础扎实,会被认为是可培养人才。大厂有完善培训机制,一些框架可以入职后再学。下面这些面试题是从字节跳动、腾讯、阿里、美团等大厂的几百份面经中挑选最高频的Java语法基础的面试题。如果这些Java语法基础(送分题)还答得不好,就会给面试官基础不牢固的印象。
Java 基础数据类型有哪些?分别占用多少个字节?
Java 的基本数据类型分为四类八种。
第一类是整数类型,包括 byte、short、int 和 long。byte 类型占用 1 个字节,它能表示的范围是 - 128 到 127。主要用于节省内存空间,例如在处理一些底层的、数据量小且范围有限的整数情况,比如在存储文件的字节流数据时可以考虑使用。short 类型占用 2 个字节,范围是 - 32768 到 32767。在一些对内存比较敏感,同时数据范围又不是很大的情况下可以使用,不过在实际开发中用得不是特别多。int 类型是最常用的整数类型,占用 4 个字节,范围是 - 2147483648 到 2147483647,在一般的计数器、数组下标等场景下广泛使用。long 类型占用 8 个字节,它能表示的范围非常大,用于存储较大的整数,比如存储时间戳(从 1970 年 1 月 1 日 00:00:00 UTC 到某个时间点的毫秒数)这种可能很大的整数值。
第二类是浮点类型,包含 float 和 double。float 类型占用 4 个字节,它用于表示单精度浮点数,在存储精度要求不是特别高的小数时使用,例如在一些简单的图形处理中表示坐标位置等。double 类型占用 8 个字节,是双精度浮点数,精度更高,在大多数科学计算、金融计算等对精度要求较高的场景下广泛使用。
第三类是字符类型 char,它占用 2 个字节,用于存储单个字符,字符在 Java 中是使用 Unicode 编码表示的,所以可以存储多种语言的字符。
第四类是布尔类型 boolean,它的取值只有 true 和 false,理论上占用 1 个字节或者 1 位(在实际的虚拟机实现中可能会有所不同),主要用于条件判断等逻辑场景。
在 Java 中基本数据类型 char 类型占几个字节?
在 Java 中,基本数据类型 char 占 2 个字节。这是因为 Java 中的 char 类型是使用 Unicode 编码来表示字符的。Unicode 是一种全球字符编码标准,它为世界上几乎所有的字符都分配了一个唯一的数字编号。
Unicode 编码最初设计是用 16 位(2 个字节)来表示一个字符,这样可以涵盖世界上大部分语言的字符。例如,英文字母、数字、标点符号等常见字符,以及汉字、日文假名、韩文等其他语言的字符都可以用 char 类型来存储。
在内存中,这 2 个字节可以存储从 0 到 65535(2 的 16 次方 - 1)的整数编号,每个编号对应一个特定的字符。例如,字符 'A' 在 Unicode 编码中有一个对应的编号,这个编号就存储在这 2 个字节中。而且,Java 提供了一系列的方法来操作 char 类型,比如可以将一个 char 类型的变量转换为对应的整数编号,也可以通过整数编号来获取对应的字符。
这种 2 字节的设计使得 Java 的 char 类型具有很强的通用性,能够方便地处理各种语言的字符,为国际化的软件应用开发提供了良好的基础。比如在开发一个多语言支持的文本编辑软件或者网站时,char 类型就可以很好地存储和处理不同语言的文本内容。
如何选择 Java 基础数据类型?
在选择 Java 基础数据类型时,需要综合考虑多个因素。
首先是数据范围。如果要处理的数据范围较小,比如在 - 128 到 127 之间的整数,byte 类型是一个很好的选择,它可以有效地节省内存空间。例如,在处理一些简单的文件加密中的字节数据或者简单的小型计数器等场景下可以使用 byte。而如果数据范围可能会超过 byte 但小于 32768,short 类型就比较合适,不过在实际开发中 short 用得相对较少。当数据范围不确定或者可能比较大,但又没有达到 long 那么大的规模时,int 类型是最常用的,像数组下标、循环计数器等场景基本都是用 int。如果数据范围非常大,例如处理一些天文数字或者时间戳等信息,long 类型就是必须的。
其次是数据精度。对于小数来说,如果精度要求不是特别高,比如在简单的图形绘制中表示一些坐标位置,float 类型可以满足要求。但如果是在科学计算、金融计算等对精度要求很高的场景下,就需要使用 double 类型。因为 double 的精度更高,可以更准确地表示小数的值。
然后是数据类型的用途。对于字符数据,就需要使用 char 类型,它用于存储单个字符,并且因为是基于 Unicode 编码,能够存储多种语言的字符。在处理文本信息,如读取文件中的字符或者在用户界面中处理字符输入输出等场景下会用到 char。而对于逻辑判断,只有 true 和 false 两种状态的情况,就使用 boolean 类型,在条件判断语句(如 if - else 语句)、循环控制(如 while 循环的条件判断)等逻辑场景下广泛使用。
另外,还需要考虑内存占用和性能。较小的数据类型通常占用较少的内存,在处理大量数据时,如果能合理选择数据类型可以节省内存空间。但也要注意,在一些处理器架构上,某些数据类型的操作可能会更快。例如,在一些 32 位的处理器上,对 int 类型的操作可能比 byte 或者 short 类型更高效,因为处理器的字长是 32 位,与 int 类型的长度相匹配。
引用数据类型有哪些?
在 Java 中,引用数据类型主要包括类(class)、接口(interface)、数组(array)和枚举(enum)。
类是引用数据类型中最常见的一种。它是面向对象编程的核心概念,用于封装数据和行为。例如,我们可以定义一个名为 Person 的类,在这个类中可以包含姓名、年龄等属性,以及说话、行走等行为(通过方法来实现)。当我们创建一个 Person 类的对象时,实际上是在内存中开辟了一块空间来存储这个对象的属性值,而变量只是引用这个对象在内存中的位置。
接口也是引用数据类型。它定义了一组方法签名,但没有具体的实现。接口主要用于定义规范,实现多态等面向对象的设计模式。比如,我们可以定义一个名为 Drawable 的接口,里面有一个 draw 方法。然后不同的类可以实现这个接口,以提供自己的绘制逻辑。
数组是一种特殊的引用数据类型,它用于存储多个相同类型的数据。
在内存中,数组对象存储了数组元素的引用,这些元素可以是基本数据类型或者引用数据类型。
枚举是一种特殊的数据类型,它允许我们定义一组命名的常量。枚举类型的值是有限的、预定义的,它提供了一种更安全、更可读的方式来处理一组固定的值,而不是使用整数或者字符串来表示这些固定的值。
面向对象编程是什么?
面向对象编程(Object - Oriented Programming,简称 OOP)是一种编程范式,它以对象为核心来组织程序代码。
在面向对象编程中,对象是类的实例。类是一种抽象的数据类型,它定义了对象的属性和行为。例如,我们可以定义一个 “汽车” 类,这个类里面可以包含汽车的各种属性,如颜色、品牌、速度等,还可以包含汽车的行为,如启动、加速、刹车等。这些属性通过变量来表示,行为通过方法来表示。当我们创建一个 “汽车” 类的对象时,就好像是在现实世界中制造了一辆具体的汽车,这个对象有自己的颜色、品牌和速度等属性,并且可以执行启动、加速和刹车等操作。
面向对象编程有三个主要的特性:封装、继承和多态。
封装是指将对象的属性和行为封装在一个类中,并且对外部隐藏对象的内部细节。这样做的好处是可以提高代码的安全性和可维护性。例如,对于汽车的速度属性,我们可以通过方法来设置和获取速度,而不是直接让外部代码访问这个属性,这样可以防止不合理的速度设置。
继承是一种代码复用的机制。它允许我们创建一个新的类(子类),这个子类继承自一个现有的类(父类)。子类可以继承父类的属性和行为,并且可以添加自己新的属性和行为。例如,我们可以定义一个 “跑车” 类,它继承自 “汽车” 类,跑车类除了具有汽车的一般属性和行为外,还可以有自己特殊的属性,如最高时速更高等。
多态是指同一种行为在不同的对象中有不同的实现方式。例如,在一个动物类层次结构中,“动物” 类有一个 “叫” 的行为。当我们创建 “狗” 类和 “猫” 类作为 “动物” 类的子类时,“狗” 的 “叫” 可能是 “汪汪” 声,“猫” 的 “叫” 可能是 “喵喵” 声。在程序中,我们可以通过父类的引用指向子类的对象,当调用 “叫” 这个行为时,会根据对象的实际类型来执行相应的行为。
面向对象编程还包括一些其他的概念,如抽象类、接口等。抽象类是一种不能被实例化的类,它主要用于定义一些抽象的方法和属性,作为其他类的基类。接口则是一种更纯粹的抽象,它只定义了一组方法签名,没有具体的实现,用于规定类的行为规范,实现多态等高级的面向对象设计。通过这些概念,我们可以构建出复杂的、易于维护和扩展的软件系统。
Java 的三大特性是什么?
Java 的三大特性是封装、继承和多态。
封装是把对象的属性和操作(方法)结合为一个独立的整体,并尽可能隐藏对象的内部细节。通过封装,外界不能直接访问对象的私有属性,只能通过公共的方法来访问和修改。例如,一个银行账户类(BankAccount),账户余额(balance)是一个私有属性,不能被外部随意修改。可以通过存款(deposit)和取款(withdraw)等公共方法来操作余额。这样做的好处是可以保证数据的安全性和一致性。如果没有封装,外部代码可以随意修改余额,可能会导致数据错误。同时,封装也使得代码的维护更加容易,因为对属性的操作都集中在类的方法中,当需要修改属性的操作逻辑时,只需要修改类内部的方法,而不用在所有使用该属性的地方进行修改。
继承是一种代码复用机制,允许创建一个新的类(子类)继承现有类(父类)的属性和方法。子类可以继承父类的非私有属性和方法,并且可以添加自己的新属性和方法。比如,有一个动物(Animal)类,它有属性如动物的名称(name)和方法如发出声音(makeSound)。然后有一个狗(Dog)类继承自动物类,狗类除了继承动物类的名称属性和发出声音的方法外,还可以添加自己特有的属性,如狗的品种(breed),并且可以重写发出声音的方法,让狗发出 “汪汪” 声。通过继承,可以构建类的层次结构,提高代码的复用性,减少代码的冗余。
多态是指同一种行为在不同的对象中有不同的实现方式。多态主要通过方法重写和方法重载来实现。方法重写是在子类中重新定义父类中已经存在的方法,当通过父类引用指向子类对象并调用重写的方法时,会执行子类中的方法。例如,动物类中有发出声音的方法,猫类和狗类继承动物类并分别重写这个方法,当使用动物类的引用指向猫类或者狗类对象并调用发出声音的方法时,会分别发出 “喵喵” 和 “汪汪” 声。方法重载是在同一个类中定义多个同名方法,但参数列表不同,根据传入的参数不同来执行不同的方法。多态使得程序具有更好的扩展性和灵活性,能够根据对象的实际类型来执行相应的操作。
Java 中的抽象类与接口有何区别?请给出一个实际例子。
在 Java 中,抽象类和接口有许多区别。
首先,抽象类是一种不能被实例化的类,它可以包含抽象方法和非抽象方法。抽象方法只有方法签名,没有方法体,它强制子类去实现这些抽象方法。非抽象方法则可以有具体的实现,子类可以直接继承这些方法。例如,有一个抽象的图形(Shape)抽象类,它有一个抽象方法计算面积(calculateArea),因为不同的图形(如三角形、圆形)计算面积的方式不同。同时,它可以有一个非抽象方法打印图形信息(printShapeInfo),这个方法在所有图形类中都有相同的基本逻辑,如打印图形的名称。
接口则是完全抽象的,它只包含方法签名,没有方法体,并且接口中的方法默认都是 public 和 abstract 的。接口主要用于定义一组规范,实现类必须实现接口中的所有方法。例如,有一个可绘制(Drawable)接口,里面有一个绘制(draw)方法。这个接口规定了任何实现它的类都必须能够实现绘制自己的功能。
在继承方面,一个类只能继承一个抽象类,这体现了 Java 的单继承原则。但是一个类可以实现多个接口,这样可以让一个类具有多种不同的行为规范。比如,有一个汽车(Car)类,它可以实现可驾驶(Drivable)接口和可维修(Repairable)接口,分别规定了汽车可以被驾驶和可以被维修的行为规范。
从设计角度看,抽象类更多地用于在一组相关的类中提取公共的属性和行为,并且可以部分实现这些行为。而接口更多地用于定义不同类之间的一种契约或者规范,让这些类能够以一种统一的方式进行交互。例如,在一个图形绘制系统中,抽象类 Shape 可以包含图形的公共属性如颜色、位置等,而接口 Drawable 可以用于规定所有能够被绘制的图形都必须实现的绘制方法,这样可以方便地对不同的图形进行绘制操作。
Java 实例化对象的方式有哪些?
在 Java 中有多种实例化对象的方式。
一种常见的方式是使用 new 关键字。例如,有一个简单的类 Person,包含姓名(name)和年龄(age)两个属性,还有一个打招呼(sayHello)的方法。
class Person { String name; int age; void sayHello() { System.out.println("我叫" + name + ",今年" + age + "岁。"); } }
可以通过以下方式实例化对象:
Person person = new Person(); person.name = "张三"; person.age = 20; person.sayHello();
这里使用 new 关键字在堆内存中为 Person 对象分配空间,然后可以通过对象引用(person)来访问对象的属性和方法。
另外,还可以通过反射来实例化对象。反射是 Java 的一个强大特性,它允许在运行时检查和操作类、方法、属性等。例如,对于上述的 Person 类,可以通过以下反射方式来实例化:
import java.lang.reflect.Constructor; try { Class<?> personClass = Class.forName("Person"); Constructor<?> constructor = personClass.getConstructor(); Person personByReflection = (Person) constructor.newInstance(); personByReflection.name = "李四"; personByReflection.age = 22; personByReflection.sayHello(); } catch (Exception e) { e.printStackTrace(); }
在这个例子中,首先通过类的全限定名(在这里假设 Person 类在默认包下)获取 Class 对象,然后获取类的构造函数,最后通过构造函数来实例化对象。反射方式在一些框架开发、动态代理等场景下非常有用,比如在 Spring 框架中,通过反射来实例化和管理 Bean 对象。
还有一种方式是通过克隆(Clone)来创建对象。要使用克隆,需要让类实现 Cloneable 接口,并重写 clone 方法。例如:
class CloneablePerson implements Cloneable { String name; int age; @Override public Object clone() throws CloneNotSupportedException { return super.clone(); } }
可以这样克隆对象:
try { CloneablePerson original = new CloneablePerson(); original.name = "王五"; original.age = 24; CloneablePerson cloned = (CloneablePerson) original.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); }
克隆可以快速创建一个与已有对象内容相同的新对象,不过对于引用类型的属性,只是复制了引用,可能需要进一步处理来实现深克隆。
重载和重写有什么区别?形参和实参的区别?重写中形参可以一样吗?重载呢?
重载(Overloading)是指在同一个类中,有多个方法具有相同的名字,但它们的参数列表不同(参数的个数、类型或者顺序不同)。例如,有一个计算面积的类 ShapeCalculator,它可以有多个计算面积的方法:一个方法用于计算圆形的面积,接收圆的半径作为参数;另一个方法用于计算矩形的面积,接收矩形的长和宽作为参数。
class ShapeCalculator { double calculateArea(double radius) { return Math.PI * radius * radius; } double calculateArea(double length, double width) { return length * width; } }
当调用这些方法时,编译器会根据传入的实际参数的类型、个数和顺序来确定调用哪一个具体的方法。重载主要是为了方便程序员使用相同的方法名来表示相似的操作,增强了代码的可读性和可维护性。
重写(Overriding)是在继承关系中发生的,子类重新定义了父类中已经存在的方法。要求方法名、参数列表和返回值类型(对于基本数据类型和 void 必须相同,对于引用数据类型可以是子类)都要和父类的方法相同。例如,有一个动物类 Animal,它有一个发出声音的方法 makeSound,而狗类 Dog 继承自 Animal 类,并重写了 makeSound 方法,让狗发出 “汪汪” 声。
class Animal { void makeSound() { System.out.println("动物发出声音"); } } class Dog extends Animal { @Override void makeSound() { System.out.println("汪汪"); } }
重写体现了多态性,使得程序能够根据对象的实际类型来执行相应的方法,这在面向对象编程中是非常重要的概念,用于实现不同对象对同一行为的不同实现。总的来说,重载是在一个类内部的多态表现,重写是在继承关系中的多态体现。
形参(形式参数)是在方法定义中声明的参数,它用于接收方法调用时传入的实际参数。例如,在一个方法定义 public void printName (String name) 中,name 就是形参,它规定了这个方法需要接收一个字符串类型的参数。实参(实际参数)是在调用方法时实际传递给方法的参数。比如,在 printName ("张三") 这个调用中,“张三” 就是实参。
在重写中,形参必须和父类中的方法形参完全一样,这是重写的规则之一。如果形参不同,那就不是重写而是定义了一个新的方法。在重载中,形参必须不同,这是判断重载的关键因素之一。因为如果形参相同,仅仅靠返回值等其他因素无法区分方法,不符合重载的定义,如前面提到的会导致编译错误。
如果返回值不同其他都一样,是可以形成重写或者重载吗?会有什么问题?
在 Java 中,如果仅仅是返回值不同而其他(方法名、参数列表)都一样,这既不是重载也不是重写。
对于重载来说,如前面所述,其判断依据是方法名相同但参数列表不同。如果只有返回值不同,编译器无法根据传入的参数来区分调用哪一个方法,这就违反了重载的规则。例如,假设有一个类有两个方法,方法名都是 test,参数都是一个整数,只是一个返回整数,一个返回字符串,当调用 test 方法并传入一个整数参数时,编译器不知道该调用哪一个方法,这会导致编译错误。
对于重写而言,规则要求方法名、参数列表和返回值类型(对于基本数据类型和 void 必须相同,对于引用数据类型可以是子类)都要和父类的方法相同。如果仅仅返回值不同,这也不符合重写的定义。这样做会导致编译器认为子类中的方法和父类中的方法是两个不同的方法,而不是重写关系。当通过父类引用指向子类对象并调用这个方法时,不会按照重写的规则执行子类中的方法,而是会执行父类中的方法,这可能会导致程序逻辑出现错误。例如,父类中有一个方法返回一个整数,子类中同名同参数的方法返回一个字符串,当通过父类引用调用这个方法时,期望得到一个整数结果,但实际得到的可能是不符合预期的字符串,破坏了程序的一致性和多态性。
Java 类加载器有哪些特点?
Java 类加载器是 Java 运行时环境(JRE)的一部分,用于将类的字节码文件加载到内存中。
首先,Java 类加载器具有层次结构。最顶层是引导类加载器(Bootstrap Class Loader),它是用本地代码实现的,主要负责加载 Java 的核心库,如 java.lang 包中的类。这些类对于 Java 程序的运行是最基础的,比如 Object 类、String 类等都是由引导类加载器加载的。由于它是使用本地代码编写,在 Java 代码中无法直接获取它的引用。
其次是扩展类加载器(Extension Class Loader),它的作用是加载 Java 的扩展库。这些扩展库通常位于 JDK 安装目录下的 jre/lib/ext 目录中。它是由 Java 代码实现的,是 Sun 公司(现在是 Oracle 公司)为了方便开发者加载一些标准扩展库而提供的加载器。
然后是应用程序类加载器(Application Class Loader),也称为系统类加载器,它主要负责加载应用程序的类路径(classpath)下的类。这是开发者最常接触的类加载器,因为它加载的是我们自己编写的类以及第三方库中的类。
Java 类加载器还有双亲委派模型的特点。当一个类加
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
17年+码农经历了很多次面试,多次作为面试官面试别人,多次大数据面试和面试别人,深知哪些面试题是会被经常问到。 在多家企业从0到1开发过离线数仓实时数仓等多个大型项目,详细介绍项目架构等企业内部秘不外传的资料,介绍踩过的坑和开发干货,分享多个拿来即用的大数据ETL工具,让小白用户快速入门并精通,指导如何入职后快速上手。 计划更新内容100篇以上,包括一些企业内部秘不外宣的干货,欢迎订阅!