【面试】Java基础

1、面向对象(oop)的理解

注重对象,只关心其使用,不关心其具体实现,即只关心对象是谁;
具有封装、继承、多态三大特性:
封装:不需关心内部实现和具体构造,只需知道如何操作。即根据职责将属性和方法封装到一个抽象的类中(如电视、手机,将内部封装起来直接使用)。
继承:实现代码的重用,相同的代码不需要重复的编写;子类继承自父类,可以直接享受父类中已经封装好的方法,不需要再次开发;子类中也应根据职责,封装子类特有的属性和方法。
多态 :不同的对象调用相同的方法,产生不同的执行结果,增加代码的灵活度(都是动物叫,猫是喵喵,狗是汪汪)。
多态存在的三个必要条件:继承重写,父类引用指向子类对象。

// 封装、继承、多态
class Person1{
    String name;
    int age;
    private int height;// 私有 封装
    public Person1(String name, int age)
    {
        this.name = name;
        this.age = age;
    }
    public void talk()
    {
        System.out.println("This is father class talk() !");
    }
    public void setHeight(int h)
    {
        if(h > 0)
            this.height = h;
    }
    public int getHeight()
    {
        return this.height;
    }
}

// 继承(只能继承单个父类) extends father_class
class Student extends Person1{ // java 一个子类只能有一个父类
    String school;
    public Student(String name, int age, String school)
    {
        super(name,age);//调用父类的构造方法,且必须放在第一行
        this.school = school;

        // super. 调用父类属性、方法
        super.name = "Ming";
        super.age = 19;

        // 私有属性不可修改
        // super.height = 178;
        super.setHeight(178);
    }

    @Override
    public void talk() { // 重写父类方法
        System.out.println("This is sub class talk() !");
        super.talk();//还可以调用父类被覆写的方法
    }
}

class test1{
    public static void main(String[] args){
        Student s1 = new Student("Michael",18,"BJTU");
        System.out.println("name: "+s1.name+", age: "+s1.age+
                ", school: "+s1.school + ", height: " + s1.getHeight());

        s1.talk();

        Person1 p = new Student("Ming",19,"BJTU");
        p.talk();// 多态,父类对象通过子类实例化,调用的是子类的talk

        Student s2 = (Student) p;//向下类型转换,需要强制,向上是自动转的
        s2.talk();// 如果 p 是由 Person1 new 出来的,此处报错
    }
}

面向对象编程:就是不对创建对象、使用对象、指挥对象做事情的过程
面向对象设计:就是在管理和维护对象之间的关系】

2、面向对象与面向过程的区别

面向过程:注重实现过程,即关注一件事该怎么做,以过程为中心。不用实例化类,所以开销较小,消耗资源较少,注重性能时用,如:单片机、嵌入式开发等。
面向对象:注重实现对象,即关注一件事由谁来做,以对象为中心。因为有三大特性,所以系统更灵活、易维护、易扩展、易复用。

3、JDK与JRE区别

JDK:JRE+开发工具集
JRE:即Java的运行环境,JVM+核心类库
如果只运行,JRE即可;要编译则需JDK
【JVM:是Java编程的核心,运行程序时,JVM负责把字节码转换为机器特定的代码】

4、值传递和引用传递
值传递:形参中传的是实参的值。方法调用时,实际参数把它的值传递给对应的形式参数,方法中执行形参值的改变不影响实际参数的值。(基本数据类型作为参数(如:String,int))

public static void changeInt(int a, int b){
	int tmp;
	tmp = a;
	a = b;
	b = tmp;
}

参数调用:

// int类型,值传递
int a = 123;
int b = 456;

printInt(a, b);
changeInt(a, b);
printInt(a, b);

执行结果:
a = 123; b = 456
a = 123; b = 456
引用传递:形参中传的是实参的地址。方法调用时,实际参数的引用(即地址)被传递给方法中相对应的形式参数,在方法中执行对形式参数的操作实际上就是对实际参数的操作(地址操作),这样方法执行中形式参数值的改变将会影响实际参数的值。(对象或数组作为参数(如:Person,Array))

class Person{
	String name;
	int age;
	
	public Person(String name, int age){
		this.name = name;
		this.age  = age;
	}
	
	public void changePerson(Person person, String name, int age){
		person.name = name;
		person.age  = age;
	}
	
	public void printPerson(Person person){
		System.out.println("name = " + person.name + "; age = " + person.age);
	}
}

参数调用:

		// 对象,引用传递
		Person p = new Person("张三", 20);
		
		p.printPerson(p);
		p.changePerson(p, "李四", 30);
		p.printPerson(p);

执行结果:
name = 张三; age = 20
name = 李四; age = 30

5、==和equals()区别

==:比较的是两个变量的数值是否相等,即比较的是存储在内存里面的数值是否相同;
但是有两种情况:
比较两个基本数据类型时:直接比较的是值是否相等
比较两个对象时:比较的是引用,比较的是内存地址是否相同,即引用是否指向同一块内存
equals():比较的是两个对象是否相同,比较的也是引用。但是equals方法可以重写覆盖,所以可以通过这样让它比较数据内容。

6、为什么重写equals时必须重写hashCode方法
equals 方法和 hashCode 方法都是是 Object 类中的两个基础方法,它们被设计为共同协作来判断两个对象是否相等。不同对象的 hashCode 可能相同,但 hashCode 不同的对象一定不相等;所以使用 hashCode 可以起到快速初次判断对象是否相等的作用。​对比两个对象是否相等时,为了提升性能,先使用 hashCode 进行比较,如果比较的结果是 true,那么就可以使用 equals 再次确认两个对象是否相等,如果比较的结果是 true,那么这两个对象就是相等的,否则其他情况就认为两个对象不相等。如果没有重写 hashCode 方法,会直接执行 Object 中的 hashCode 方法,而 Object 中的 hashCode 方法对比的是两个不同引用地址的对象,所以结果会是 false。
(可用于:解决相等的自定义对象存储在 Set 集合发生冲突的问题)

7、深拷贝与浅拷贝

将一个对象的引用复制给另外一个对象的方法:
1、直接赋值 2、浅拷贝 3、深拷贝
直接赋值:二者的引用是同一个对象,并没有创建出一个新的对象,所以两者改一个,另一个对象的值也随之改变。

public class cloneDemo {
    public static void main(String[] args) {
        Person p = new Person("小明",11,123);
        Person p1 = p;
        System.out.println(p);
        System.out.println(p1);
    }
}

打印结果:
Others.base.cloneDemo.Person@4554617c
Others.base.cloneDemo.Person@4554617c
浅拷贝:简单的复制拷贝的操作。创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。被拷贝对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。因此如果其中一个对象改变了这个地址,就会影响到另一个对象。
注意实现对象拷贝的类,必须实现Cloneable接口,并覆写clone()方法.

p1.setCode(1234);
Person p2 = p;
p2.setName("小明克隆人");
p2.setAge(12);
p2.setCode(12345);
System.out.println(p);
System.out.println(p2);

打印结果:
Person{name=‘小明’, age=11, code=1234}
Person{name=‘小明克隆人’, age=12, code=12345}
深拷贝:在堆区重新申请空间,进行拷贝操作。被拷贝对象的所有变量都含有与原来的对象相同的值,除去那些引用其他对象的变量。那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象。换言之,深复制把要复制的对象所引用的对象都复制了一遍。
每个被引用的类都要实现Cloneable接口并重写clone

protected Object clone()   {
    try {
        // super.clone();
        // return super.clone();

        // 改为深复制:
        Person newPerson = (Person)super.clone();
        // 拷贝一份新的address
        newPerson.address = (Address) address.clone();
        return newPerson;
    } catch (CloneNotSupportedException e) {
        e.printStackTrace();
    }
    return null;
}

打印结果:
Person{name=‘小明’, age=11, code=1234, address=Student{name=‘我是一个小学生’}}
Person{name=‘小明’, age=11, code=1234, address=Student{name=‘我是一个小学生’}}

Person{name=‘小明’, age=11, code=1234, address=Student{name=‘我是一个小学生’}}
Person{name=‘小明克隆人’, age=12, code=12345, address=Student{name=‘我是一个小学生克隆人’}}

深克隆与浅克隆在内存中区别:
在这里插入图片描述
【拓展:还可以利用串行化来做深复制】

8、A a = new A();在内存中做了哪些事情/即在JVM层面实例一个对象的步骤

1)在栈内存为变量a分配内存空间;
2)在堆内存为A对象开辟空间;
3)对A对象进行初始化;
4)把对象地址赋给a变量(即将变量a指向A对象);
其实3)初始化具体步骤为:对对象A的成员变量进行默认初始化->显式初始化->构造方法对对象成员变量赋值

9、重载和重写的区别

都是实现多态的方式,区别在于重载实现的是编译时的多态性,而重写实现的是运行时的多态性。
重载:发生在同一个类中,方法名相同参数列表不同(类型、个数、顺序不同),与方法返回值和访问修饰符无关,即重载的方法不能根据返回类型进行区分。
重写:发生在父子类中,方法名、参数列表必须相同,返回值小于等于父类,抛出的异常小于等于父类,访问修饰符大于等于父类(里氏代换原则);如果父类方法访问修饰符为private则子类中就不是重写。

10、多态的理解

父类引用指向子类对象,只能调用执行子类对象中重写的父类中的方法

11、String为何不可变?不可变好处

为何:String 类中使⽤ final 关键字修饰字符数组来保存字符串,jdk9后变成使用byte数组。
好处:缓存hash值(使得hash值不变,只进行一次计算);保证线程安全的,不需要利用特殊机制来保证同步问题,因为对象的值无法改变。可以降低并发错误的可能性,因为不需要用一些锁机制等保证内存一致性问题,减少了同步开销。

12、String对象有几种创建方式?

两种。

String s1 = "abc";
String s2 = new String("abc");

第一种:仅仅是一个赋值语句,在创建的时候,JVM 会检查在字符串池中,是否已经存在该字符串,如果已经存在了,那么会返回这个字符串的引用给变量 s1。如果不存在,那么会创建一个 abc 字符串对象,再赋值给 s1。因此,这句话可能只创建 1 个或者 0 个对象。
第二种:在内存中创建 1 个或者 2 个对象。如果 abc 字符串已经在字符串池中存在了,那么就不需要在创建 abc 字符串的对象了,但是 new String 这行代码会再构造出一个和 abc 一样的字符串,并且是放在堆上。(把 new String(“abc”) 这句话拆成两个部分来看,一个是”abc”, 另一个是 new String()。)

13、String,StringBuffer和StringBuilder之间的区别是什么?

可变性:String是不可变对象,任何对String修改都会创建新的String对象,StringBuffer和StringBuilder可变类。
效率/速度:频繁对字符进行操作时,使用String会生成一些临时对象,多一些附加操作,效率低些。
安全性:Stringbuffer方法由synchronized修饰,线程安全。
如:

1 String s = "abcd";
2 s = s+1;
3 System.out.print(s);// result : abcd1

JVM是这样解析这段代码的:首先创建对象s,赋予一个abcd,然后再创建一个新的对象s用来执行第二行代码,也就是说我们之前对象s并没有变化,所以我们说String类型是不可改变的对象了,由于这种机制,每当用String操作字符串时,实际上是在不断的创建新的对象,而原来的对象就会变为垃圾被回收掉。
StringBuffer与StringBuilder就不一样了,他们是字符串变量,是可改变的对象,每当我们用它们对字符串做操作时,实际上是在一个对象上操作的,这样就不会像String一样创建一些另外的对象进行操作了,当然速度就快了。

14、8种基本数据类型与占内存大 小
byte(1字节 -2的7次方~2的7次方-1)、short(2字节 -2的15次方到2的15次方-1)、int(4字节)、float(4)、double(8)、long(8字节),char(2)、booean(4-内存对齐),
逻辑上boolean型只占1bit,但是虚拟机底层对boolean值进行操作实际使用的是int型,操作boolean数组则使用byte型

15、float 与 double/short与int
float f = 1.1;错
float f=1.1f; 对

short s=1;
s+=1;
s=s+1;错
s++; (隐式类型转换 相当于 s = (short) (s + 1);)

16、Java集合机制与使用场景

集合框架的三大接口:set(无序 不能放重复元素) list(有序 挨着排 可以放重复元素) map(成键值对存)

17、set与list区别?

1)都是继承自Collection接口
2)元素:
List:元素放入有序,元素可重复。
Set:元素无放入顺序,元素不可重复,重复的元素会被覆盖掉。(元素在set中的位置是由该元素的HashCode决定的,其位置的固定的,加入Set的Object必须定义equals方法)
3)访问:
List:支持for循环,也就是通过下标来遍历,也可用迭代器。
Set:只能用迭代,因为它无序,无法用下标来取得想要的值。
4)效率:
List:和数组类似,可动态增长,查找效率高,插入删除效率低,因为会引起其他元素位置改变。
Set:检索元素效率低下,删除和插入效率高,插入删除不会引起元素位置改变。

18、ArrayList扩容机制

添加元素时使用 ensureCapacityInternal() 方法来保证容量足够。
如果不够时,需要使用 grow() 方法进行扩容,新容量大约是旧容量的 1.5 倍左右。
扩容操作需要调用 Arrays.copyOf() 把原数组整个复制到新数组中,这个操作代价很高。
modCount记录结构修改次数。

19、ArrayList 与 Vector 区别呢?为什么要⽤Arraylist取代Vector呢

1)实现:都实现List接口,
2)线程安全:Vector 使用了 Synchronized 来实现线程同步,是线程安全的;而 ArrayList 是非线程安全的。
3)性能:ArrayList 在性能方面要优于 Vector。
4)扩容:ArrayList 和 Vector 都会根据实际的需要动态的调整容量,只不过在 Vector 扩容每次变2倍,而 ArrayList 变1.5倍
5)长度:ArrayList默认初始长度10

20、ArrayList和LinkedList的区别

1)实现接口:
都实现List接口,arrayList还实现了RandomAccess接口,代表支持随机访问
2)线程安全问题:
ArrayList与LinkedList都是线程不同步的,也就是都不保证线程安全。
3)底层数据结构:
ArrayLis底层使用数组,默认初始大小为10;插入元素超出则会动态扩容为原来1.5倍;
LinkedList底层采用双向链表数据结构(注意:JDK1.6之前为循环链表,JDK1.7取消了循环)。
4)查询:数组支持随机快速访问,而链表需要依次遍历,更耗时。
5)占用内存空间大小:一般LinkedList占空间更大,双向列表每个结点要维护两个指针。但是若ArrayList刚到扩容阈值,扩容后会浪费很多空间。
6)数组查找原理:ArrayList空间连续,查询通过偏移量找;LinkList底层链表,逻辑连续,空间不连续,指针访问。

21、HashMap 和 Hashtable 的区别

1)线程安全:
Hashtable方法sychonized修饰,线程安全;HashMap 不;
2)效率方面:
由于Hashtable方法被sychonized修饰,效率比HashMap低
3)底层数据结构:
HashMap jdk8当链表长度>=8并且数组长度>=64链表会转红黑树,Hashtable没有这样机制。
4)初始容量与扩容:
默认初始量:Hashtable为11,HashMap为16;
若指定初始量:Hashtable用指定的值,HashMap会扩充为 2 的幂次⽅⼤⼩。
扩容:Hashtable容量变为原来2n+1倍,HashMap变为2倍。
5)对Null key与Null value支持:
HashMap 中,null 可以作为键,这样的键最多可以有一个,但可以有一个或多个键所对应的值为 null;
在 Hashtable 中,键和值都不能为 null,否则会直接抛出 NullPointerException空指针异常。

22、HashSet怎样检查重复
通过计算对象的hashcode定位,同时比较与其他对象hashcode是否相等,若没有相符的则假设没有重复对象;若有相同的则equlas判断。

23、HashMap与HashSet区别
HashSet:实现了Set接口,不允许集合中出现重复元素。在将对象存储在HashSet之前,要确保重写hashCode()方法和equals()方法,这样才能比较对象的值是否相等,确保集合中没有储存相同的对象。如果不重写上述两个方法,那么将使用下面方法默认实现:public boolean add(Object obj)方法用在Set添加元素时,如果元素值重复时返回 “false”,如果添加成功则返回"true"
HashMap:实现了Map接口,Map接口对键值对进行映射。Map中不允许出现重复的键(Key)。
HashMap是非线程安全的(非Synchronize),要想实现线程安全,那么需要调用collections类的静态方法synchronizeMap()实现。
public Object put(Object Key,Object value)方法用来将元素添加到map中。
在这里插入图片描述
Map接口有两个基本的实现:TreeMap和HashMap。TreeMap保存了对象的排列次序,而HashMap不能。HashMap可以有空的键值对(Key(null)-Value(null))

24、HashMap在jdk8与jdk7区别
JDK7中的HashMap:基于链表+数组实现,底层维护一个Entry数组

Entry<K,V>[] table;

根据计算的hashCode将对应的KV键值对存储到该table中,一旦发生hashCode冲突,那么就会将该KV键值对放到对应的已有元素的后面, 此时,形成了一个链表式的存储结构,如下图:
在这里插入图片描述
JDK8中的HashMap:基于数组+链表+红黑树的方式实现,底层维护一个Node数组

Node<K,V>[] table;

当链表的存储的数据个数大于等于8的时候,不再采用链表存储,而采用了红黑树存储结构。这是JDK7与JDK8中HashMap实现的最大区别。
如下图所示:
在这里插入图片描述
不同点:
1)发生hash冲突时
JDK7:发生hash冲突时,新元素插入到链表头中,即新元素总是添加到数组中,旧元素移动到链表中。
JDK8:发生hash冲突后,会优先判断该节点的数据结构是红黑树还是链表,如果是红黑树,则在红黑树中插入数据;如果是链表,则将数据插入到链表的尾部并判断链表长度是否大于8,如果大于8要转成红黑树。
2)扩容时
插入:扩容resize()时,JDK7中链表的插入是用的头插法,而JDK8中则改为了尾插法
因为JDK1.7是用单链表进行的纵向延伸,当采用头插法时会容易出现逆序且环形链表死循环问题。但是在JDK1.8之后是因为加入了红黑树使用尾插法,能够避免出现逆序且链表死循环的问题。
扩容顺序:JDK7中是先扩容再添加新元素,JDK8中是先添加新元素然后再扩容
扩容条件:JDK8中数组扩容的条件也发了变化,只会判断是否当前元素个数是否超过了阈值,而不再判断当前put进来的元素对应的数组下标位置是否有值。

25、HashMap 的⻓度为什么是2的幂次⽅

(注意:&与:两个为1才为1 ^异或:相同则为0,不相同则为1)
因为当容量为2的幂时,hash&(length-1) 运算才等价于length取模,也就是h%length,而&比%具有更高的效率,也就是计算机会计算的更快。
而且这样能尽量均匀分布减少哈希冲突:
2的n次方实际就是1后面n个0,2的n次方-1实际就是n个1。这样按位“与”时,每一位都能&1,真正参与了运算,分布更均匀。

26、为什么要把key的哈希码右移16位呢

因为hash值是int类型占4字节正好32位,为了使计算出的hash值更加的分散,所以选择先将hash右移16位,然后再与h异或时,就能达到h的高16位和低16位都能参与计算,尽最大努力减少哈希冲突

27、Java中接口和抽象类的区别?

方法与实现:接口只有定义,不能有方法的实现,java 1.8中可以定义default与static方法体;抽象类可以有定义与实现,方法可在抽象类中实现。
成员变量:接口成员变量只能是public static final的,且必须初始化,抽象类可以和普通类一样任意类型。
继承实现:一个类只能继承一个抽象类(extends),可以实现多个接口(implements)
都不能实例化
接口不能有构造函数,抽象类可以有

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值