java面试
01_谈谈对面向对象思想的理解
这个问题通常会让很多人有点不知所措。我觉得我一直在编码,但很难解释我的想法。
下面,我来谈谈自己的想法,
首先,我们来谈谈面vs面向对象
我认为这两者是思维角度的差异,面向过程更多的是执行者从的角度思考问题,面向对象更多的是组织者例如,我想在0-10之间产生一个随机数,以面向过程的思维,然后我更注重如何设计算法,然后确保0-10的随机数相对平衡,面对对象的思维会更加关注,我找谁帮我们做这件事,比如Random类,调用提供的方法。
因此,面向对象的思维更多的是考虑如何选择合适的工具,然后组织一起做一件事。
例如,如果一个导演想拍一部电影,他必须先有男猪脚和女猪脚,然后还有其他的,最后组织这些资源,拍一部电影。
回到我们的程序世界,组织者的思维无处不在,例如,我们想开发项目,开发三层架构模式,所以在这个时候,我们不需要重复轮子,只需要选择市场上的主流框架,例如SpringMVC,Spring,MyBatis,这些都是各层的主流框架。
JDK,JRE,JVM有什么区别?
JDK:Java Development Kit,Java开发工具包,提供了Java的开发环境和运行环境。包含了编译Java编译源文件Javac,还有调试和分析的工具。
JRE:Java Runtime Environment,Java运行环境,包含Java虚拟机和一些基本类库
JVM:Java Virtual Machine,Java虚拟机,提供执行字节码文件的能力
所以,如果只是运行Java只需安装程序JRE即可。
另外注意,JVM是实现Java但是JVM不是跨平台的,
不同的平台需要安装不同的平台JVM
[外链图片存储失败,源站可能有防盗链机制,建议保存图片直接上传(img-h8TBD4aT-1631606888922)(…/images/v2-199da3b71771a2ac8a6abca871d2bca2_720w.jpg)]
浅谈Java常量池
一、概述
- :确定了编译期,*.class文件的一部分包括字面量(Literal)和符号引用(Symbolic Reference)。
- :文字字符串,声明为final的常量值(int / long / double…)等。
- :名称和接口的完全限制(Fully Qualified Name)、字段的名称和描述符(Descriptor)、方法的名称和描述符。
- :方法区的一部分,jvm类装载操作完成后,将完成类装载操作class常量池在文件中载入内存并保存在方法区。
- JDK1.字符串常量池位于方法区。 JDK1.7字符串常量池已移至堆。 JDK1.8字符串常量池位移到元空间。
二、字符串常量池
字符串常量池是常量池运行的一部分
String s1 = "Hello"; String s2 = "Hello"; String s3 = "Hel" "lo"; String s4 = "Hel" new String("lo"); String s5 = new String("Hello"); String s6 = s5.intern(); String s7 = "H"; String s8 = "ello"; String s9 = s7 s8; System.out.println(s1 = s2); // true
System.out.println(s1 == s3); // true
System.out.println(s1 == s4); // false
System.out.println(s1 == s9); // false
System.out.println(s4 == s5); // false
System.out.println(s1 == s6); // true
java中==比较的是内存地址
-
s1 == s2 (true),s1、s2赋值时均使用的字符串字面量"Hello",在编译期间,这种字面量会直接放入class文件的常量池中,载入运行时常量池后,s1、s2指向的是同一个内存地址。
-
s1 == s3 (true),s3虽然是动态拼接出来的字符串,但所有参与拼接的部分都是已知的字面量,在编译期间,这种拼接会被优化,因此String s3 = “Hel” + “lo”;在class文件中被优化成String s3 = “Hello”;,所以s1 == s3成立。
-
s1 == s4 (false),s4虽然也是拼接出来的,但new String(“lo”)这部分不是已知字面量,编译器不会优化,必须等到运行时才可以确定结果,所以s4指向堆中某个地址。
-
s1 == s9 (false),s9是s7、s8两个变量拼接,都是不可预料的,编译器不作优化,运行时拼接成新字符串存于堆中某个地址。
-
s4 == s5 (false),二者都在堆中,但地址不同。
-
s1 == s6 (true),这两个相等完全归功于intern()方法,s5在堆中,内容为"Hello" ,intern方法会尝试将"Hello"字符串添加到常量池中,并返回其在常量池中的地址,因为常量池中已经有了"Hello"字符串,所以intern方法直接返回地址;而s1在编译期就已经指向常量池了,因此s1和s6指向同一地址,相等。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jzwAKopO-1631606888925)(…/images/9643870-b42fcf0c4e6b4918.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O4ItggZa-1631606888926)(…/images/9643870-d05c492b6e0d506d.png)]
==和equals的区别
== 是判断两个变量或实例是不是指向同一个内存空间,equals() 是判断两个变量或实例所指向的内存空间的值是不是相同
== 是指对内存地址进行比较 , equals() 是对字符串的内容进行比较
== 指引用是否相同, equals() 指的是值是否相同
==.
比较基本的数据类型:比较的是数值.
比较引用类型:比较引用指向的值(地址).
equals
自定义的类,如果需要比较的是内容,那么就要学String,重写equals方法
代码案例:测试以下的每道题,你是否能够正确得到答案?
String s1 = new String("zs");
String s2 = new String("zs");
System.out.println(s1 == s2);//false
String s3 = "zs";
String s4 = "zs";
System.out.println(s3 == s4);//ture
System.out.println(s3 == s1);//false
String s5 = "zszs";
String s6 = s3+s4;
System.out.println(s5 == s6);//false 因为字符串是不可变的对象,拼接字符串相当于new了一个对象了
final String s7 = "zs"; //使用final了说明是常量,都在常量池
final String s8 = "zs";
String s9 = s7+s8; //使用final了说明是常量,常量和常量拼接,不会new一个对象
System.out.println(s5 == s9);//ture
final String s10 = s3+s4; //s3+s4依然是new的,s10只能赋值一次
System.out.println(s5 == s10);//false
final的作用
final修饰类,表示类不可变,不可继承: 比如,String,不可变性
final修饰方法,表示该方法不可重写:比如模板方法,可以固定我们的算法
final修饰变量,这个变量就是常量
注意:
修饰的是基本数据类型,这个值本身不能修改
final Student student = new Student(1,"Andy");
student.setAge(18);//注意,这个是可以的!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-csxubhfK-1631606888928)(…/images/image-20210325093939077.png)]
String s = "java"与String s = new String(“java”)
String s = “java”;
String s = new String(“java”);
这两者的内存分配方式是不一样的。
第一种方式,JVM会将其分配到常量池,而第二种方式是分配到堆内存.
String s = new String(“abc”)到底产生几个对象?
栈只是对堆中或常量池中对象的引用,new String(“abc”) 的过程中,会检查常量池中是否有abc,如果没有,在常量池中进行创建,并且在堆中new 一个 string为abc的对象,如果有,只是在堆中new 一个 string为abc的对象。所以可能是一个,也可能是两个.
String,StringBuffer,StringBuilder区别.
什么是线程安全? 一个对象是线程安全的,当通过多线程去访问这个对象的时候,你不需要做额外的操作(额外的同步控制),他依然能表现出正确的数据,就叫做线程安全。
String 跟其他两个类的区别是
String是final类型,每次声明的都是不可变的对象, 所以每次操作都会产生新的String对象,然后将指针指向新的String对象。
StringBuffer,StringBuilder都是在原有对象上进行操作
所以,如果需要经常改变字符串内容,则建议采 用这两者。
StringBuffer vs StringBuilder
前者是线程安全的,后者是线程不安全的。 线程不安全性能更高,所以在开发中,优先采用StringBuilder. StringBuilder > StringBuffer > String(性能排序)
什么时候。我们会考虑线程安全的问题?
当单个线程去访问资源的时候,不需要去考虑线程安全。
只有当多个线程同时访问这个资源的时候,才需要考虑线程安全。(因为会出现边界的问题)(多线程访问同一个资源才需要考虑)
开发中,你用StringBuilder来解决什么问题?
用来解决字符拼接的问题,在方法内写 StringBuilder.append(" ");
StringBuilder sb = new StringBuilder(); sb.append(" ");
每个线程访问一个StringBuilder,不会出现线程安全问题 【局部变量每个线程都独享。根本不存在线程安全的问题】
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r8xPY51b-1631606888930)(…/images/image-20210325101003101.png)]
我们常说的CURD指的是什么?
CRUD是指在做计算处理时的增加(Create)、更新(Update)、读取(Retrieve)和删除(Delete)几个单词的首字母简写,CRUD主要被用在描述软件系统中数据库或者持久层的基本操作功能。
接口和抽象类的区别
这个问题,要分JDK版本来区分回答:
JDK1.8之前:
语法:
抽象类:方法可以有抽象的,也可以有非抽象, 有构造器
接口:方法都是抽象,属性都是常量,默认有public static final修饰.
设计:
抽象类:同一类事物的抽取,比如针对Dao层操作的封装,如,BaseDao,BaseServiceImpl
接口:通常更像是一种标准的制定,定制系统之间对接的标准
例子:
1,单体项目,分层开发,interface作为各层之间的纽带,在controller中注入IUserService,在Service注入IUserDao
2,分布式项目,面向服务的开发,抽取服务service,这个时候,就会产生服务的提供者和服务的消费者两个角色
这两个角色之间的纽带,依然是接口
JDK1.8之后:
接口里面可以有实现的方法,注意要在方法的声明上加上default或者static
最后区分几个概念:
多继承,多重继承,多实现
多重继承:A->B->C(爷孙三代的关系)
多实现:Person implements IRunable,IEatable(符合多项国际化标准)
多继承:接口可以多继承,类只支持单继承
抽象类 定义: 在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。简单来说,使用关键字 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.");
}
}
相关面试题 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 修饰。
算法题-求N的阶乘
这道算法题一般考查的递归的编程技能,那么我们回顾下递归程序的特点:
1,什么是递归?
递归,就是方法内部调用方法自身 递归的注意事项: 找到规律,编写递归公式 找到出口(边界值),让递归有结束边界 注意:如果递归太多层,或者没有正确结束递归,则会出现“栈内存溢出Error”! 问题:为什么会出现栈内存溢出,而不是堆内存溢出?
2,这道题该怎么写?
规律:N!=(n-1)!*n;
出口:n1或n0 return 1;
public static int getResult(int n){
if(n<0){
throw new ValidateException("非法参数");
}
if(n==1 || n==0){
return 1;
}
return getResult(n-1)*n;
}
算法题-求解斐波那切数列的第N个数是几?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KzOt0mZw-1631606888931)(…/images/v2-43a9a8e07aca193bc79500b36d8d0804_720w.jpg)]
public static int getFeiBo(int n) {
if (n < 0) {
return -1;
}
if (n == 1 || n == 2) {
return 1;
} else {
return getFeiBo(n - 1) + getFeiBo(n - 2);
}
}
Int和Integer的区别(重点)
Integer i1 = new Integer(12);
Integer i2 = new Integer(12);
System.out.println(i1==i2);//false
Integer i3 = 126; //JDK1.5后 自动装箱的操作,查看源码可得,如果数据在(-128,127)的数据范围内,则不会new
Integer i4 = 126;
/* //反编译获取的代码 Integer.valueOf(123); public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); } * */
int i5=126;
System.out.println(i3==i4); //true
System.out.println(i3==i5); //true 自动拆箱,因为是数值,所以是相等的
Integer i6 = 128; //根据源码可得,此处就等于,Integer i6 = new Integer(128);
Integer i7 = 128;
int i8=128;
System.out.println(i6==i7);//false
System.out.println(i6==i8); //true 自动拆箱,数值
以上这些输出的答案是什么?true or false? why?
你可以自己先思考,再看后面的答案分析。
答案揭晓
分情况来比较
- 都定义为Integer的比较:
new:一旦new,就是开辟一块新内存,结果肯定是false 不new: 看范围 Integer做了缓存,-128至127,当你取值在这个范围的时候,会采用缓存的对象,所以会相等 当不在这个范围,内部创建新的对象,此时不相等
- Integer和int的比较:
实际比较的是数值,Integer会做拆箱的动作,来跟基本数据类型做比较 此时跟是否在缓存范围内或是否new都没关系
源码分析:
当我们写Integer i = 126,实际上做了自动装箱:Integer i = Integer.valueOf(126); 分析这段源码
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
//IntegerCache是Integer的内部类
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
static {
// high value may be configured by property
int h = 127;
自动装箱和自动拆箱:
手动装箱过程:
Integer a = new Integer(66); //手动装箱过程:使用Integer包装类就相当于把数字装进箱子里一样
手动拆箱过程:
int i = a.intValue(); //手动拆箱过程:把Integer包装类型的数据转换成int类型的基本数据,intValue()就是将Integer转换为int
自动装箱过程:
Integer b = 88; //自动装箱过程,自动将数值转成Integer类型的数据。
自动拆箱过程:
int d = b; //自动拆箱过程,自动将数值转换成int类型的数据。
Integer i3 = Integer.valueOf(55); // Integer的valueOf()函数的功能就是把int类型转换为Integer。
java基本数据类型范围
整型:
byte:-2^7 ~ 2^7-1,即-128 ~ 127。1字节。Byte。末尾加B
short:-2^15 ~ 2^15-1,即-32768 ~ 32767。2字节。Short。末尾加S
有符号int:-2^31 ~ 2^31-1,即-2147483648 ~ 2147483647。4字节。Integer。
无符号int:0~2^32-1。
long:-2^63 ~ 2^63-1,即-9223372036854774808 ~ 9223372036854774807。8字节。Long。末尾加L。(也可以不加L)
浮点型:
float:4字节。Float。末尾加F。(也可以不加F)
double:8字节。Double。
字符型:
char:2字节。Character。
布尔型:
boolean:Boolean。
方法的重写和重载的区别
一般出现在(笔试题-选择题),下面我们说下重点
- 重载:发生在一个类里面,方法名相同,参数列表不同(混淆点:跟返回类型没关系)
以下不构成重载 public double add(int a,int b) public int add(int a,int b)
- 重写:发生在父类子类之间的,方法名相同,参数列表相同
List和Set的区别
这简直是一道送分题,简单到我都不好意思写出来,但居然有人会搞错,汗!
- List(有序,可重复)【 ArrayList LinkList】
- Set(无序,不可重复)【HashSet、TreeSet】
无序 !=可以排序
这里的无序:指的是,加进去的顺序,不一定等于输出的顺序。
例子:
Collections 和 Collection 的区别?
在Java里面,工具类的命名,会+s结尾,Collections是工具类,Collection是接口
谈谈ArrayList和LinkedList的区别
ArrayList,数组,连续一块内存空间 LinkedList,双向链表,不是连续的内存空间
虽然不严谨,但也可以应付很多面试了
ArrayList,查找快,因为是连续的内存空间,方便寻址,但删除,插入慢,因为需要发生数据迁移 LinkedList,查找慢,因为需要通过指针一个个寻找,但删除,插入块,因为只要改变前后节点的指针指向即可。
思考后的答案:
ArrayList连续的内存空间,可计算的偏移量LinkedList不连续,无法计算,一个个往下找ArrayList>LinkedList<