目录
背景
21年4月,应聘杭州滨江某自研公司,以下是该公司的笔试题
题目的答案,是根据我的理解写的。可能有不对的地方,欢迎大家指正。
这一场的面试题忘了,当时没记录
题目
1、Object的常用方法
●1.getClass()
public final native Class<?> getClass()
获取对象运行时的class对象,通常和反射机制搭配使用。
class对象就是描述对象所属类的对象
●2.hashCode
public native int hashCode()
获取对象的散列值。Object中该方法默认返回的是对象的堆内存地址。
●3.equals
public boolean equals(Object object){
return (this == obj);
}
比较2个对象,如果2个对象引用的是同一个对象,那么返回true。
一般equals和==是不一样的,但在Object中是一样的。子类一般重写equals方法。
●4.clone
protected native Object clone() throws CloneNotSupportedException;
该方法是保护方法,实现对象的浅拷贝,只有实现了Cloneable接口才可以调用该方法,否则抛出CloneNotSupportException。
默认的clone是浅拷贝。浅拷贝,指的是对象内属性引用的对象只会拷贝引用地址,而不会将引用的对象重新分配内存。
深拷贝就是会连引用的对象也重新创建。
●5.toString
public String toString(){
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
返回一个String对象,一般子类都有覆盖。
默认返回格式:对象的class名称 + @ + hashCode的十六进制字符串。
●6.notify
public final native void notify()
final方法,主要唤醒在该对象上等待的某个线程
●7.notifyAll
public final native void notifyAll()
final方法,唤醒在该对象上等待的所有线程
●8.wait(long timeout)
public final native void wait(long timeout) throws InterruptedException
timeout是毫秒值
使当前线程等待该对象的锁,当前线程必须是该对象的拥有者,也就是具有该对象的锁。
这里设置一个超时间隔,如果在规定时间没有获得到锁就返回。
无参的wait() 方法会一直等待,直到获得锁或者被中断。
●9.wait(long timeout, int nanos)
public final void wait(long timeout, int nanos) throws InterruptedException{
if (timeout < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos >= 500000 || (nanos != 0 && timeout == 0)) {
timeout++;
}
wait(timeout);
}
timeout:最大等待时间,毫秒
nanos:附加时间在毫秒范围(0-999999),纳秒/毫微秒,十亿分之一秒
该方法导致当前线程等待,直到其他线程调用此对象的 notify() 方法或notifyAll()方法,或在指定已经过去的时间。此方法类似于 wait 方法的一个参数,但它允许更好地控制的时间等待一个通知放弃之前的量。实时量,以毫微秒计算,计算公式如下:1000000 * timeout + nanos
在所有其他方面,这种方法与 wait(long timeout) 做同样的事情。特别是 wait(0, 0) 表示和 wait(0) 相同。
●10.wait()
public final void wait() throws InterruptedException{
wait(0);
}
实际调用的是wait(long timeout)方法,只不过timeout是0。
●11.finalize
protected void finalize() throws Throwable{}
是保护方法,用于在GC的时候再次被调用。如果实现了这个方法,对象可能在这个方法中再次复活,避免被GC回收。
2、 == 和 equals 的区别是什么?
==
对于基本类型和引用类型 == 的作用效果是不同的,如下所示:
基本类型:比较的是值是否相同;
引用类型:比较的是引用是否相同;
代码示例:
String x = "string";
String y = "string";
String z = new String("string");
System.out.println(x==y); // true
System.out.println(x==z); // false
System.out.println(x.equals(y)); // true
System.out.println(x.equals(z)); // true
代码解读:
因为 x 和 y 指向的是同一个引用,所以 == 也是 true。
而 new String()方法则重写开辟了内存空间,所以 == 结果为 false,
而 equals 比较的一直是值,所以结果都为 true。
equals
equals 本质上就是 ==,只不过 String 和 Integer 等重写了 equals 方法,把它变成了值比较。看下面的代码就明白了。
首先来看默认情况下 equals 比较一个有相同值的对象,代码如下:
String s1 = new String("老王");
String s2 = new String("老王");
System.out.println(s1.equals(s2)); // true
进入 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 重写了 Object 的 equals 方法,把引用比较改成了值比较。
总结 :
== 对于基本类型来说是值比较,对于引用类型来说是比较的是引用;
equals 默认情况下是引用比较,只是很多类重新了 equals 方法,比如 String、Integer 等把它变成了值比较,
所以一般情况下 equals 比较的是值是否相等。
3、以下代码的运行结果
public class Stu {
private String name;
private int age;
public Stu(String name, int age) {
super();
this.name = name;
this.age = age;
}
public static void main(String[] args) {
Stu s1 = new Stu("张三", 18);
Stu s2 = new Stu("张三", 18);
System.out.println(s1 == s2);// false
System.out.println(s1.equals(s2));// false
}
}
4、以下代码的运行结果
String s0 = null;
String s1 = "Tomcat";
String s2 = "Tom" + "cat";
String s3 = new String("Tomcat");
System.out.println(s1==s2);//true
System.out.println(s1.equals(s2));//true
System.out.println(s1==s3);//false
System.out.println(s1.equals(s3));//true
//intern():返回常量池中这个对象的引用
System.out.println(s1==s3.intern());//true
System.out.println(s0==s1);//false
System.out.println(s0.equals(s1));//空指针异常!
//null去equals某个值,就会报错。如果非null去equals任意值,就不会错,如下所示
System.out.println(s1.equals(s0));//false
5、String, StringBuilder,StringBuffer
String:定义的不可变字符串。在操作少量数据时使用。
StringBuilder:定义的可变字符串。线程不安全。性能比StringBuffer快。推荐单线程下使用。
StringBuffer:定义可变字符串。线程安全,因为它的所有公开方法都用synchronized修饰。推荐多线程下用。
整体性能上:StringBuilder > StringBuffer > String
为什么是这样的性能排序?
String每执行一次+(重载运算符),就要创建一个新的对象
StringBuffer比StringBuilder相比,少了同步锁。
为什么不可变?
String底层代码中,整个类被final修饰,该类不能被继承;其中的value[]属性被final修饰,引用不能被修改。
为什么StringBuffer线程安全?
StringBuffer通过synchronized关键字修饰,保证资源不会被抢,从而确保了线程安全。
6、ArrayList和LinkedList
①ArrayList底层是数组,(动态数组),LinkedList底层是双向链表。
②随机访问get和set,ArrayList要优于LinkedList。
③对于新增和删除,add/remove,LinkedList更快,因为ArrayList要移动数据。
7、一些常用的线程安全的集合类
①Vector:(实现了List接口),比ArrayList多了同步化机制
②Stack:栈,继承于Vector。先进后出。
③HashTable:比HashMap多了线程安全。(实现了Map接口)
④ConcurrentHashMap:高效且线程安全的集合。(继承AbstractMap,实现了ConcurrentMap接口)
8、以下代码的运行结果
static class VO implements Cloneable {
List<Object> datas = new ArrayList<>();
@Override
public VO clone() throws CloneNotSupportedException {
return (VO) super.clone();
}
}
@Test
public void cloneT() throws CloneNotSupportedException {
VO vo = new VO();
vo.datas.add("1");
VO vo2 = vo.clone();
vo2.datas.add("2");
System.out.println(vo.datas.size());// 2
System.out.println(vo2.datas.size());// 2
}
9、完成下面的代码
public class VO {
private Integer id;
public VO(Integer id) {
super();
this.id = id;
}
//...get、set、toString方法
}
List<VO> volist = mockDatas();
要求:请对集合voList,根据id进行分组
分析:就是有个集合,集合内装的是某种对象(泛型),对象有个ID字段。
根据ID进行分组,同个ID的对象放在一起。
实现代码如下
public static void main(String[] args) {
// 根据题意,准备的测试用的voList
VO v1 = new VO(18);
VO v2 = new VO(18);
VO v3 = new VO(18);
VO v4 = new VO(18);
List<VO> voList = new ArrayList<>();
voList.add(v1);
voList.add(v2);
voList.add(v3);
voList.add(v4);
//解法1
// 0.新建map,存放分组后的vo集合数据
Map<Integer, List<VO>> map = new HashMap<Integer, List<VO>>();
// 1.遍历list
for (VO vo: voList) {
// 2.获取每个元素的id,判断map中的key有没有这个id
if (map.containsKey(vo.getId())) {
// 有,就把这个元素放到这个key下的value里
map.get(vo.getId()).add(vo);
} else {
// map的key里没有这个id,
// ==>那就以此新建key,再把对应的vo集合放进value
List<VO> newVOList = new ArrayList<VO>();
newVOList.add(vo);
map.put(vo.getId(), newVOList);
}
}
// 现在,已经得到了按id分组的map。
// 下一步,根据map,新建list,把分好组的数据放进去
List<List<VO>> resultList = new ArrayList<List<VO>>(map.values());
// 展示结果
for (List<VO> list : resultList) {
for (VO vo : list) {
System.out.println("User---" + vo.toString());
}
}
/*
* HashMap.values(): 获取value集合
* 把Map转换成List<List<User>>,为什么会变成List<List<User>>?
* 原因在于
* map是以多个userId为键值,存储多个List<User>,map.values转换成list就会变成
* List<List<User>>
*/
// 解法2——使用Stream流
Map<Integer, List<VO>> finalMap = voList.stream().collect(Collectors.groupingBy(VO::getId));
}
10、写一个线程安全的单例模式
单例模式,一种设计模式,一个类只能创建一个对象。
单例模式,可以有饿汉式 和 懒汉式。其中饿汉式,是线程安全的。
懒汉式:
(1)为节省内存,创建对象的步骤放到getInstance()中
(2)为保证单例,创建对象时加判断。若未创建,就创建;若已创建,就返回之前创建的
(3)为线程安全,方法上加锁,用synchronized上锁
//处理后的懒汉式(线程安全), 缺点效率低,
public static Singleton{
private static Singleton singleton;
private Singleton(){
}
public static synchronized Singleton getInstance(){
if(singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
//没有用synchronized 的懒汉式, 线程不安全
public static Singleton{
private static Singleton singleton;
private Singleton(){
}
public static Singleton getInstance(){
if(singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
饿汉式
(1)私有化构造器
(2)内部创建一个实例
(3)定义一个方法将实例返回。
这个方法的修饰符必须是public static,返回对象也static修饰。
(4)返回对象用private修饰,防止外界随意调用。
//饿汉式
public class Singleton{
private static Singleton singleton = new Singleton();
private Singleton(){
}
public static Singleton getInstance(){
return singleton;
}
}
11、哪些方法保障线程安全
使用synchronized关键字
同步代码块
synchronized 锁对象{
多条操作共享数据的语句
}
锁对象:任意对象,多个线程必须使用同一把锁
这种方法的好处:多线程下提高安全性
坏处:运行速率变低
同步方法
●同步非静态方法:
格式:将synchronized写在方法上。
锁对象:当前类对象this
●同步静态方法:
锁对象:当前的类对象,类名.class
使用lock
lock():加锁 unlock():解锁
一般使用ReturnLock,是lock的实现类。
即,Lock lock = new ReturnLock
释放锁必须在finally中执行,因为锁必须释放
12、算法,任选一个
(1)合并2个有序数组,写出代码实现
(2)从一个有序数组中找一个缺少的元素。数组中至少有一个缺失元素。如数组[1,2,4,5,6,7,],缺少数字3
13、数据库编程
学生表stu(id,name,age,class_id)
学生的成绩表score(id,stu_id,kemu,chengji)
统计每个班级(class_id)的人数
select class_id as '班级', count(id) as '人数'
from stu
group by class_id
查询所有18岁以上学生的信息。如果有课程成绩,查询出最高分的课程成绩
select stu.*, score.kemu, score.chengji
from stu stu
left join score
on stu.id = score.stu_id
where stu.age > 18
查询每门课程最高分的学生信息
select b.kemu,b.chengji, c.age, c.name
from (
select kemu, max(chengji) maxc
from score group by kemu) a,score b, stu c
where
a.kemu = b.kemu and
a.maxc = b.chengji and
b.stu_id = c.id
14、介绍下几种join
15、基本类型和包装类型的区别
(1)包装类型可以为null,基本类型不可以
使得包装类型可以应用于POJO中,而基本类型不行。
(2)包装类型可用于泛型,基本类型不可以
(3)基本类型比包装类型更高效
基本类型在栈中存储具体的数值。包装类型存储的是堆中的引用.
(4)自动装箱和自动拆箱
基本类型 -> 包装类型,自动装箱
包装类型 -> 基本类型,自动拆箱