1. 面向对象和面向过程的区别
- 面向过程:面向过程的性能比面向对象是要高的,虽然面向过程也需要分配内存,计算内存偏移量,但是面向对象类调用时的实例化开销更大,更消耗资源。
- 面向对象:因为面向对象存在封装,继承,多态的特性,所以面向对象易扩展,易维护,易复用,但是性能相较于面向过程要低一些。
而Java性能差的根本原因是java是半编译语言,最终的执行代码不是可以直接被cpu运行的二进制机械码,而大多数面向过程语言的最终执行代码都是直接编译成机械码被cpu执行。
2. 关于jvm jdk jre的解答
JVM:
是运行java字节码(.class)的虚拟机,针对不同的系统存在特定的实现,目的是为了跨平台。
java程序从源代码(.java)到运行的步骤:
以前,.class–>机器码的过程中,jvm类加载器首先加载字节码文件,然后通过解释器逐行解释执行。这种方法执行速度相对缓慢,而且,有些方法和代码块是需要被经常调用的(热点代码)
后来,引进了JIT编译器,JIT属于运行时编译,当JIT编译器完成第一次编译后,其会将热点代码的字节码对应的机器码保存,用于下次直接使用。这种情况下,热点代码就属于编译执行而不是解释执行了。
JDK & JRE
- jdk:包含了jre,还有编译器javac和工具javadoc,jdb等。
- jre:java运行环境,它是运行已编译java程序所需要的所有内容的合集,包括JVM,Java类库,java命令和一些基础构建。
3. Java和c++的区别
- java不提供指针来直接访问内存,程序内存更加安全
- java只能单继承(可以多实现接口),c++支持多重继承
- java由内存管理机制,c++需要手动释放内存
4. 字符型常量和字符串常量有什么区别
- 形式上:字符型常量 ‘A’ ,字符串常量 “ABC”
- 含义上:字符型常量相当于一个整形值(ASCII),可以参与表达式运算;字符串常量代表一个内存地址
- 占内存大小:字符型通常只占2个字节,字符串常量占若干个字节
5. 构造器是否可以被override(重写)
继承时就讲过,父类的私有属性和构造方法不能被继承,SB问题
6. 重载和重写的区别
- 重载:发生在同一个类中,方法名字必须相同,参数不同,返回值和修饰符可以不同,参考构造方法。
- 重写:发生在父子类中,方法名字,参数列表必须相同,返回值和抛出异常的范围欸必须小于等于父类,访问修饰符范围必须大于父类,如果父类访问修饰符为private,则子类不能重写该方法。
7. Java面向对象编程三大特性:封装 继承 多态
- 封装
封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法。 - 继承
继承是使用已存在的类(父类)的定义作为基础建立新类(子类)的技术,拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有。子类也可以对父类进行扩展。通过使用继承我们能够非常方便地复用以前的代码。 - 多态
多态简单来说就是一个接口,因为使用不同的实例而执行不同的操作。
比较难理解所以通过一个继承实现多态的实例进行解释:
父类Employee
class Employee{
private String name;
private String addr;
private int id;
public void mailCheck(){
System.out.println("邮寄支票给:"+this.name + " "+this.addr);
}
public Employee() {
}
public Employee(String name, String addr, int id) {
System.out.println("Employee 构造");
this.name = name;
this.addr = addr;
this.id = id;
}
public get and set ...
}
子类Salary 继承了 Employee,获得了Employee的所有属性和方法,增加了属性sal,修改了方法mailCheck
class Salary extends Employee {
private double sal;
public Salary(String name, String addr, int num, double sal) {
super(name, addr, num);
this.sal = sal;
}
@Override
public void mailCheck() {
System.out.println("Sal类的mailCheck");
System.out.println("邮寄支票给:"+ getName() +" "+ sal);
}
public get and set ...
}
- 执行Demo,实例化了两个Salary对象,一个使用Salary引用s,一个使用Employee引用e。
- 当调用s.mailCheck()时,编译器在编译时会在Salary类中寻找mailCheck(),执行过程 JVM 就调用 Salary 类的 mailCheck()。
- 当调用e.mailCheck()时,编译器在编译时会在Employee类中寻找mailCheck(),也就是先检查父类中是否存在该方法,如果不存在,则编译错误;如果有,则调用子类的同名方法。所以执行过程JVM 就调用 Salary 类的 mailCheck()。
也就是我们常说的,编译看左边,运行看右边
public class Demo11 {
public static void main(String[] args) {
Salary s = new Salary("员工A","beijing",3,3600.00);
Employee e = new Salary("员工B","shanghai",2,2400.00);
s.mailCheck();
e.mailCheck();
}
}
执行结果
Employee 构造
Employee 构造
Sal类的mailCheck
邮寄支票给:员工A 3600.0
Sal类的mailCheck
邮寄支票给:员工B 2400.0
8. String StringBuffer和StringBuilder的区别是什么?String为什么是不可变的?
可变性
String类中使用final关键字修饰字符数组来保存字符串,private final char value[ ],所以String对象是不可变的。
而StringBuilder与StringBuffer都继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组保存字符串,char[ ] value,但是没有用final关键字修饰,所以这两种对象都是可变的。
线程安全性
String中的对象是不可变的,也就可以理解为常量,线程安全。
AbstractStringBuilder是StringBuilder与StringBuffer的公共父类,定义了一些字符串的基本操作,如expandCapacity,append,insert,indexOf等公共方法。 StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder并没有对方法进行加同步锁,所以是非线程安全的。
性能
每次对String类型进行改变的时候,都会生成一个新的String对象,然后将指针指向新的String对象。
StringBuffer/Builder每次都会对其对象本身进行操作,而不是生成新的对象并改变对象引用。StringBuilder相比使用StringBuffer仅能获得10%~15%左右的性能提升,但却要冒多线程不安全的风险。
对于三者使用的总结:
操作少量的数据:适用String
单线程操作字符串缓冲区下操作大量数据:适用StringBuilder
多线程操作字符串缓冲区下操作大量数据:适用StringBuffer
9. 在一个静态方法内调用一个非静态成员为什么是非法的?
静态方法属于整个类,而不是某个对象,也就是被该类下的所有对象共享。静态成员可以使用类名直接访问,不需要实例化类来调用。
简单来说,把类比作一个设计图,静态方法或属性作为该设计图的一部分;把实例对象比作完成品。我们不能在设计图中找到任何完成品的属性,因为每一个完成品都是不同的。
10. 在Java中定义一个不做事且没有参数的构造方法的作用
回想以下我们在通过类创建一个对象时,会通过调用它的构造方法完成创建。如果该类是某个父类的子类,还会在构造方法中super()隐式调用父类的无参构造方法。
且如果父类中只定义了特定的有参构造方法,没有重载无参构造(如果不添加构造的情况下,会默认隐式创建一个无参构造),而在子类的构造方法中没有用super()调用父类特定的构造方法,则编译时将发生错误,因为Java程序在父类中找不到无参构造方法执行。
11. 接口和抽象类的区别
参数 | 抽象类 | 接口 |
默认的方法实现 | 可以有非抽象的方法实现 | 接口只定义抽象方法,接口的所有方法默认会隐式加上public abstract(JDK1.8 可以在接口中定义静态方法) |
实现 | 子类使用extends关键字继承抽象类,且必须重写抽象父类的抽象方法 | 子类使用关键字 implements 来实现接口。它需要提供接口中所有声明的方法的实现 |
与普通java类的区别 | 抽象类不能实例化 | 接口可以实例化,但接口并不是一个类,它和类同属于java数据类型的引用类型 |
构造器 | 抽象类可以有构造器 | 接口不是类不存在构造器 |
访问修饰符 | 抽象方法可以有public protected default 修饰符,但不能使用private,因为抽象方法目的就是为了重写 | 接口方法默认修饰符是 public,不可以使用其它修饰符 |
main方法 | 接口中可以有main,所以能够运行抽象类 | 接口没有 main 方法,因此不能运行接口 |
效率 | 比接口效率高 | 接口是稍微有点慢的,因为它需要时间去寻找在类中实现的方法 |
变量 | 任意 | 接口中所有变量必须为static final |
12. 成员变量和局部变量区别
属性 | 成员变量 | 局部变量 |
语法形式 | 成员变量属于类 | 局部变量是在方法中定义的变量或方法的参数 |
访问修饰符 | 成员变量可以被各种修饰符修饰 | 局部变量不能被访问控制修饰符以及静态修饰,除了final |
存储方式 | 属于实例对象,存在于堆内存 | 存在于栈内存 |
生存时间 | 随对象的创建而存在 | 随方法的调用而消失 |
赋值 | 成员变量会根据其类型默认赋值 | 局部变量不会自动赋值 |
13. 创建一个对象用什么运算符?对象实体与对象引用有何不同?
new运算符,new创建对象实例并存储在堆内存,对象引用则指向对象实例(对象引用存放在栈内存)
比较难理解所以通过实例说明:
public class Demo{
public Demo{}//默认构造
}
接下来用Demo类创建一个对象
Demo demo = new Demo();
这个类实例化语句包含了4个动作:
- 右边的new Demo是以Demo类为模板,在堆内存中创建了一个Demo对象
- 末尾的()意味着,在对象创建后,立即调用Demo类的构造函数,对刚生成的对象进行初始化
- 左边的Demo demo创建了一个Demo类的引用变量demo,它存放在栈内存,用于指向Demo对象
- "="操作符使对象引用指向刚创建的Demo对象的内存地址
一个对象引用可以指向0-1个对象;一个对象实体可以有n个引用指向它。
14. 什么是方法的返回值?返回值在方法中的作用
方法的返回值是我们获取到某个方法体中代码执行后产生的结果。
其作用是接收结果,使其可以用于其他操作
15. 一个类的构造方法作用?若一个类没有声明构造方法,程序能否正确执行?
构造方法主要是完成对类对象的初始化工作,可以参考13题中对象实体在堆内存的创建过程。
一个类即使没有显式声明构造方法也会存在默认无参构造方法。
16. 构造方法特性
无返回值
名字与类名相同
生成类的对象时自动执行,不需要调用
17. 静态方法和实例方法的区别
属性 | 静态方法 | 实体方法 |
外部调用 | 通过"类名.方法名"/“对象名.方法名” | 通过"对象名.方法名" |
访问范围 | 静态方法在访问本类成员时,只能访问静态成员 | 实例方法无限制 |
18. 对象的相等和指向它们的引用相等,两者有何不同
对象的相等,比较的是内存中存放的内容是否相等。
对象的引用相等,比较的是内存中的地址是否相等。
19. 在调用子类构造方法之前会先调用父类的无参构造,其目的
帮助子类做初始化工作
20. ==与equals
==:判断两个对象地址是否相等,是否为同一个对象(基本数据类型比较值,引用数据类型比较地址)
equals():
- 情况1:类没有override equals方法,那么它将继承Object类的equals方法
boolean equals(Object o){
return this==o;
}
这说明,如果一个类没有override equals方法,默认的equals也就等同于"=="
- 情况2:类覆盖了 equals() 方法。一般我们都通过override equals()来比较两个对象的内容是否相等;若相等返回ture。
String的equals方法是被重写的,因为object的equals()是比较对象的内存地址,而String的equals()经过重写后比较的是对象的值
//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类型的对象时,JVM会在常量池中查找是否有存在和要创建的值相同的字符串对象,如果有就把它赋给当前引用,如果没有就在常量池重新创建。
21. hashCode 与 equals。为什么重写equals时必须重写hashCode方法?
hashCode():
其作用时获取哈希码,实际上返回的是一个int整数,这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 在定义JDK的Object中,这就意味着Java中的任何类都包含有hashCode的函数。
//这是Object类中定义的hashCode方法,且是一个本地方法
public native int hashCode();
为什么要使用hashCode:
以HashSet如何查重为例,说明为什么需要hashCode:当把对象加入到HashSet时,HashSet会先计算对象的hash值判断对象加入的位置,同时也会与其他已经加入的对象的hash值进行比较,如果没有相同的hash值,HashSet会假设对象没有重复;但如果发现有相同hash值的对象,会调用equals()检查hash值相等的对象是否相同。如果相同则不让其加入,如果不同则重新散列到其他位置。
想象以下,如果没有hashCode,在HashSet加入对象的时候,需要一个一个地进行对象equals()比较,这样效率就会非常低。现在我们只需要通过比较hashCode就可以完成HashSet的查重操作。
通过我们可以看出:hashCode() 的作用就是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode()在散列表中才有用,在其它情况下没用。在散列表中hashCode() 的作用是获取对象的散列码,进而确定该对象在散列表中的位置。
hashCode()与equals()的相关规定
1:如果两个对象的hash值相等,两个对象不一定相同
2:如果两个对象相同,hash值一定相同
3:两个对象相等,对两个对象调用equals()返回true
4:如果重写了equals,则hashCode也必须重写
5:hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)