面向对象和面向过程的区别?
面向对象和面向过程是两种不同的编程范式,它们在设计和实现软件时有着不同的理念和方法。面向对象更适合大型、复杂的项目,尤其是需要维护和扩展的系统;而面向过程更适合小型、线性的任务或对性能要求较高的情况。下面是两者的主要区别:
面向对象编程
数据封装:数据和操作数据的函数被封装在类中,形成对象。这种封装隐藏了数据的具体实现细节,只暴露出必要的接口供外部调用。
继承性:允许创建类的层次结构,子类可以继承父类的属性和方法,这有助于代码的复用和模块化。
多态性:同一个接口或方法名可以有多种实现方式,具体实现取决于调用它的对象类型。这增加了代码的灵活性和扩展性。
抽象:提供抽象类和接口,允许定义行为的规范而不提供具体的实现,从而支持更高级别的设计。
强调对象:关注于对象之间的交互,每个对象都有自己的状态和行为,通过消息传递来实现功能。
面向过程编程
数据与函数分离:数据和处理数据的过程通常是分开的,没有像 OOP 那样严格的封装。
流程控制:面向过程的编程更侧重于算法和步骤的顺序执行,通常使用函数(或子程序)来组织代码,但这些函数并不一定绑定到特定的数据上。
简单和直接:对于简单的任务,面向过程的编程可能更为直观和容易理解,因为它直接描述了问题的解决步骤。
不支持继承和多态:语言通常不支持继承和多态这样的概念,虽然可以通过其他方式(如函数重载)实现类似的效果。
Java 语言有哪些特点?
简单性:Java 的设计目的是使语言简单且易于学习。自动的垃圾回收机制,减少了程序员对内存管理的负担。
跨平台性:Java 代码被编译成字节码,由 Java 虚拟机(JVM)解释执行,实现“一次编写,到处运行”的理念。
面向对象:Java 是一种纯面向对象的语言,支持封装、继承和多态。
安全性:Java 通过严格的类型检查、禁止指针访问、代码验证等机制提高了安全性。
多线程:Java 内置了对多线程的支持,允许多个线程并发执行,提高了程序的响应性和资源利用率。
动态性:Java 支持运行时动态加载类和动态链接,使得代码可以在运行时进行修改和扩展。
健壮性:Java 的异常处理机制帮助开发者捕获和处理运行时错误,增强程序的稳定性和可靠性。
支持网络编程:Java 提供了丰富的网络通信库,如 Socket 编程,支持网络应用的开发。
编译和解释性:Java 源代码被编译成字节码,由 JVM 解释执行或通过即时编译器(JIT)转换为本地代码。
JVM JDK 和 JRE 有什么区别?
JVM
JVM 是一个虚拟机,用于执行 Java 字节码。它是一个抽象的计算机,提供了运行 Java 程序所需的基本运行环境,包括:
字节码解释器:读取并解释执行 Java 字节码。
垃圾回收器:自动管理内存,回收不再使用的对象所占用的空间。
安全管理器:确保运行中的应用程序不会违反安全策略。
JIT 编译器:将频繁执行的字节码编译成本地机器代码,以提高运行效率。
JRE
JRE 包含了 JVM 和运行 Java 程序所需的类库,是运行 Java 应用程序的基础。当你想要在一台计算机上运行 Java 程序时,只需要安装 JRE 即可。JRE 不包含开发工具,因此无法用来编译 Java 源代码。
JDK
JDK 是完整的 Java 软件开发工具包,它包含了 JRE 和额外的开发工具,例如:
Java 编译器(javac):用于将 Java 源代码编译成字节码。
Java 调试器(jdb):用于调试 Java 程序。
文档生成工具(javadoc):用于从源代码注释中生成 API 文档。
打包工具(jar、zip):用于创建和管理 Java 归档文件。
其他工具:如 java、javap(反汇编器)、jps(进程状态工具)等
Java 面向对象编程三大特性: 封装 继承 多态
封装
封装是将数据(变量)和操作数据的方法(函数)组合在一个单独的单元(类)中,并对外部隐藏对象的内部状态和实现细节。封装的好处在于:
隐藏内部实现:通过设置访问修饰符(如 private、protected 和 public),可以控制哪些成员变量和方法对外界可见,哪些不可见。
增强安全性:封装保护了数据不受外部非法访问和修改,通过提供公共的 getter 和 setter 方法,可以对外部访问施加控制。
简化接口:封装后的类提供了一个清晰的接口,外界仅需关注如何使用类提供的方法,而无需关心其实现细节。
继承
继承允许创建新类(子类)继承现有类(父类)的属性和方法,从而促进代码的重用和扩展。继承的好处包括:
代码重用:子类可以直接使用父类的成员变量和方法,避免了重复编写相同的代码。
层级关系:继承可以建立类之间的层级关系,使得代码结构更加清晰,易于理解和维护。
多态性基础:继承为多态性奠定了基础,因为子类对象可以被视为父类对象。
多态
多态是指一个接口或方法名可以有多种实现方式,具体实现取决于调用它的对象类型。多态分为静态多态和动态多态:
静态多态:通常指的是方法的重载(Overloading),即在同一个类中定义多个同名方法,但参数列表不同。
动态多态:也称为运行时多态,通常指的是方法的重写(Overriding),即在子类中重写父类的方法,具体调用哪个方法取决于对象的实际类型。
java 字符型常量和字符串常量的区别?
字符型常量
- 定义:字符型常量在 Java 中表示单个字符,使用一对单引号(’ ')包围,其数据类型是
char
。 - 存储:字符型常量在内存中占据两个字节的空间(Java 的
char
类型是 16 位的),存储的是该字符的 Unicode 编码。 - 使用:字符型常量主要用于存储和操作单个字符,比如在循环中逐个字符处理字符串,或者在字符数组中存储和检索字符。
字符串常量
定义:字符串常量是由零个或多个字符组成的不可变序列,使用一对双引号(" ")包围,其数据类型是 String
类的对象。
存储:字符串常量在 Java 中是不可变的(immutable),这意味着一旦创建,其内容就不能更改。字符串字面量会被存储在字符串常量池中,以节省内存和提高性能。如果相同的字符串字面量多次出现,它们将共享同一块内存空间。
使用:字符串常量通常用于处理文本数据,如用户输入、文件路径、数据库查询语句等。由于 String
类提供了丰富的字符串操作方法,如 concat()
, substring()
, replace()
, equals()
等,因此非常适合文本处理任务。
主要区别
类型:字符型常量是基本数据类型 char
,而字符串常量是引用数据类型 String
类的对象。
长度:字符型常量只能表示单个字符;而字符串常量可以表示任意长度的字符序列。
可变性:字符型常量的值是固定的,不可改变;字符串常量在创建后也是不可变的,但可以使用 String
类的方法创建新的字符串对象。
构造器 Constructor 是否可被 override?
在 Java 中,构造器(Constructor)不能被重写(override),但可以被重载(overload)。
public class Person {private String name;private int age;// 无参构造器public Person() {}// 带两个参数的构造器public Person(String name, int age) {this.name = name;this.age = age;}
}
重载和重写的区别?
重载和重写是多态性的两个重要方面,它们分别应用于不同的场景:
重载
定义:重载是在同一个类中定义多个同名的方法,但这些方法的参数列表必须不同。参数列表的不同可以是参数的数量、类型或顺序不同。
作用:重载提供了一种使用相同方法名而根据传入参数的不同执行不同行为的方式,提高了代码的可读性和整洁度。
编译时决策:重载的选择是在编译时确定的,根据传入的实际参数类型和数量,编译器会决定调用哪个方法。
重写
定义:重写发生在子类中重新定义父类的虚方法(非 final 的实例方法)。子类方法必须与父类方法具有完全相同的方法签名(方法名、参数列表和返回类型),并且子类方法不能比父类方法有更严格的访问级别。
作用:重写允许子类提供与父类相同方法的不同实现,以适应子类特有的行为,这是实现运行时多态的关键。
运行时决策:重写的选择是在运行时确定的,根据对象的实际类型,JVM会调用相应的子类或父类方法。
区别点 | 重载方法 | 重写方法 |
---|---|---|
发生范围 | 同一个类 | 子类 中 |
参数列表 | 必须修改 | 一定不能修改 |
返回类型 | 可修改 | 一定不能修改 |
异常 | 可修改 | 可以减少或删除,一定不能抛出新的或者更广的异常 |
访问修饰符 | 可修改 | 一定不能做更严格的限制(可以降低限制) |
发生阶段 | 编译期 | 运行期 |
静态方法内可以直接调用非静态成员吗?
在 Java 中,静态方法不可以直接访问非静态成员(包括非静态变量和非静态方法)。这是因为静态方法属于类本身,而不是类的实例。当静态方法被调用时,不需要创建类的任何实例,因此没有上下文来访问非静态成员,因为非静态成员是在类的实例被创建后才存在的。
可以通过以下是几种方式实现:
创建实例并调用
在静态方法内部创建类的实例,然后通过该实例访问非静态成员。
public class MyClass {private int myVar;public MyClass() {myVar = 10;}public void nonStaticMethod() {System.out.println("Non-static method called.");}public static void staticMethod() {MyClass instance = new MyClass();System.out.println(instance.myVar); // 访问非静态变量instance.nonStaticMethod(); // 调用非静态方法}
}
作为参数传递
将非静态成员作为参数传递给静态方法。
public class MyClass {private int myVar;public void setMyVar(int var) {this.myVar = var;}public int getMyVar() {return myVar;}public static void staticMethod(MyClass instance) {System.out.println(instance.getMyVar());}
}public class Main {public static void main(String[] args) {MyClass obj = new MyClass();obj.setMyVar(20);MyClass.staticMethod(obj);}
}
构造方法有哪些特性?
名称与类相同: 构造方法的名字必须与它所在的类名完全相同,这是构造方法与其他普通方法最显著的区别。
无返回类型: 构造方法没有返回类型声明,即使是 void
也不行。这是因为它用于创建和初始化对象,而不是返回一个值。
自动调用: 每当使用 new
关键字创建一个类的新实例时,构造方法会被自动调用。如果没有显式定义构造方法,Java 编译器会默认提供一个无参构造方法。
可以重载: 在同一个类中可以定义多个构造方法,只要它们的参数列表不同即可。这被称为构造方法重载(Constructor Overloading),允许以不同的方式初始化对象。
初始化对象状态: 构造方法的主要目的是初始化对象的状态。在构造方法中,你可以设置成员变量的初始值,执行一些初始化操作,如打开文件、连接数据库等。
调用父类构造方法: 子类构造方法可以调用父类构造方法,这通常通过使用 super()
关键字来完成。super()
必须是子类构造方法的第一条语句,如果没有显式调用,Java 默认调用父类的无参构造方法。
访问权限: 构造方法可以有 public
、protected
、private
或 default
(包私有)访问修饰符。这决定了谁可以使用构造方法来创建对象。
不能被继承或重写: 构造方法不能被子类重写(Override),但可以被重载(Overload)。子类可以通过 super()
调用父类的构造方法,但这并不是重写。
实例化前调用: 构造方法在对象实例化之前被调用,因此它是初始化对象的最早机会。
隐式调用与显式调用: 如果一个类没有定义任何构造方法,Java 会隐式提供一个默认的无参构造方法。但是,一旦定义了任何构造方法,哪怕是无参的,Java 就不会提供默认的构造方法,这时如果需要无参构造方法,必须显式定义。
为什么类需要定义一个空构造方法?
Java 程序在执行子类的构造方法之前,如果没有用 super()来调用父类特定的构造方法,则会调用父类中“没有参数的构造方法”。因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用 super()来调用父类中特定的构造方法,则编译时将发生错误,因为 Java 程序在父类中找不到没有参数的构造方法可供执行。解决办法是在父类里加上一个不做事且没有参数的构造方法。
接口和抽象类的区别是什么?
接口
完全抽象:接口是完全抽象的,不能有实例,接口中声明的所有方法默认是抽象的(在 Java 8 及以后版本中可以有默认方法和静态方法)。
多继承:一个类可以实现多个接口,这提供了多继承的能力,因为在 Java 中,一个类只能继承一个抽象类。
成员变量:接口中的成员变量默认是 public static final
的,意味着它们是常量。
方法的访问修饰符:接口中的方法默认是 public
的。
实现:一个类通过使用 implements
关键字来实现接口,必须实现接口中声明的所有抽象方法,除非该类本身也被声明为抽象类。
抽象类
部分抽象:抽象类可以包含抽象方法(没有实现体的方法)和具体方法(有实现体的方法),抽象类可以有实例,尽管不能直接实例化抽象类,但可以实例化其非抽象子类。
单继承:一个类只能继承一个抽象类,不能同时继承多个抽象类。
成员变量:抽象类可以有各种类型的成员变量,包括实例变量和静态变量。
方法的访问修饰符:抽象类中的方法可以是 public
、protected
、private
或 default
。
继承:一个类通过使用 extends
关键字来继承抽象类,如果该类不是抽象类,则必须实现抽象类中所有的抽象方法。
如何在面试中-画龙点睛
Java平台的理解?
典型回答
Java本身是一种面向对象的语言,最显著的特性有两个方面,一是所谓的“书写一次,到处运行”,能够非常容易地获得跨平台能力;另外就是垃圾收集,Java通过垃圾收集器回收分配内存,大部分情况下,程序员不需要自己操心内存的分配和回收。
考点分析
问题是有点笼统的,题目本身是非常开放的,往往考察的是多个方面,比如,基础知识理解是否很清楚;是否掌握Java平台主要模块和运行原理等。很多面试者会在这种问题上吃亏,稍微紧张了一下,不知道从何说起,就给出个很简略的回答。对于这类笼统的问题,你需要尽量表现出自己的思维深入并系统化,Java知识理解得也比较全面。毕竟明白基本组成和机制,是日常工作中进行问题诊断或者性能调优等很多事情的基础,相信没有招聘方会不喜欢“热爱学习和思考”的面试者。
点睛
回归正题,对于Java平台的理解,可以从很多方面简明扼要地谈一下,例如:Java语言特性,包括泛型、Lambda等语言特性;基础类库,包括集合、IO/NIO、网络、并发、安全等基础类库。对于我们日常工作应用较多的类库,面试前可以系统化总结一下,有助于临场发挥。
或者谈谈JVM的一些基础概念和机制,比如Java的类加载机制,常用版本JDK(如JDK 8)内嵌的Class-Loader,例如Bootstrap、 Application和Extension Class-loader;类加载大致过程:加载、验证、链接、初始化(这里参考了周志明的《深入理解Java虚拟机》,非常棒的JVM上手书籍);自定义Class-Loader等。还有垃圾收集的基本原理,最常见的垃圾收集器,如SerialGC、Parallel GC、 CMS、 G1等,对于适用于什么样的工作负载最好也心里有数。
比如以类加载展开谈
类加载的过程。
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段。其中验证、准备、解析3个部分统称为连接。
加载
“类加载”过程的一个阶段,在加载阶段,虚拟机需要完成以下3件事情:
通过一个类的全限定名来获取定义此类的二进制字节流。
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。
验证
连接阶段的第一步,这一阶段的目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。从整体上看,验证阶段大致上会完成下面4个阶段的检验动作:文件格式验证、元数据验证、字节码验证、符号引用验证。
准备
该阶段是正式为类变量(static修饰的变量)分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这里所说的初始值“通常情况”下是数据类型的零值。
解析
该阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这7类符号引用进行。
初始化
到了初始化阶段,才真正开始执行类中定义的Java程序代码。在准备阶段,变量已经赋过一次系统要求的初始零值,而在初始化阶段,则会根据程序员通过程序制定的主观计划去初始化类变量和其他资源。
我们也可以从另外一种更直接的形式来表达:初始化阶段是执行类构造器()方法的过程。() 不是程序员在 Java 代码中直接编写的方法,而是由 Javac 编译器自动生成的。
() 方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。
Java 虚拟机中有哪些类加载器?
Java 虚拟机中有哪些类加载器?
从 Java 虚拟机的角度来讲,只存在两种不同的类加载器:
一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分;
另一种就是所有其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader。
从Java开发人员的角度来看,绝大部分Java程序都会使用到以下3种系统提供的类加载器。
1)启动类加载器(Bootstrap ClassLoader):
这个类加载器负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。
2)扩展类加载器(Extension ClassLoader):
这个加载器由sun.misc.LauncherKaTeX parse error: Undefined control sequence: \lib at position 34: …负责加载<JAVA_HOME>\̲l̲i̲b̲\ext目录中的,或者被jav…AppClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
我们的应用程序都是由这3种类加载器互相配合进行加载的,如果有必要,还可以加入自己定义的类加载器。这些类加载器之间的关系一般如图所示。
什么是双亲委派模型?
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
为什么使用双亲委派模式?
1)使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系。
2)如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个java.lang.Object 的类,并放在程序的 ClassPath 中,那系统中将会出现多个不同的 Object 类,Java 类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。
有哪些场景破坏了双亲委派模型?
目前比较常见的场景主要有:
1)线程上下文类加载器,典型的:JDBC 使用线程上下文类加载器加载 Driver 实现类
2)Tomcat 的多 Web 应用程序
3)OSGI 实现模块化热部署
为什么要破坏双亲委派模型?
原因其实很简单,就是使用双亲委派模型无法满足需求了,因此只能破坏它,这边以面试常问的 Tomcat 为例。
我们知道 Tomcat 容器可以同时部署多个 Web 应用程序,多个 Web 应用程序很容易存在依赖同一个 jar 包,但是版本不一样的情况。例如应用1和应用2都依赖了 spring ,应用1使用的 3.2.* 版本,而应用2使用的是 4.3.* 版本。
如果遵循双亲委派模型,这个时候使用哪个版本了?
其实使用哪个版本都不行,很容易出现兼容性问题。因此,Tomcat 只能选择破坏双亲委派模型。
其它
关注公众号【 java程序猿技术】获取八股文系列文章