Java面试题(一)

本文主要探讨了Java面试中常见的几个关键知识点,包括乐观锁和悲观锁的实现方式,final对象的初始化,HashMap与HashTable的区别,数组去重的策略,以及SQL优化的一些重要方法。在乐观锁中,讲解了version和CAS操作的实现。悲观锁则强调了其线程安全性。HashMap与HashTable的主要区别在于线程安全性和性能。此外,文章还提到了SQL优化的重要性,并给出了若干优化实例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.乐观锁和悲观锁的具体实现

乐观锁

总是认为不会产生并发问题,每次去取数据的时候总认为不会有其他线程对数据进行修改,因此不会上锁,但是在更新时会判断其他线程在这之前有没有对数据进行修改,一般会使用版本号机制或CAS操作实现。

version方式实现乐观锁

一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值加1,。当线程A要更新数据值时,在读取数据同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。

核心SQL代码:

update table set x=x+1, version=version+1 where id=#{id} and version=#{version};

CAS操作方式

即compare and swap 或者 compare and set,涉及到三个操作数,数据所在的内存值,预期值,新值。当需要更新时,判断当前内存值与之前取到的值是否相等,若相等,则用新值更新,若失败则重试,一般情况下是一个自旋操作。

悲观锁

总是假设最坏的情况,每次取数据都认为其他线程会修改,所以都会加锁(读锁、写锁、行锁等),当其他线程想要访问数据时,都需要阻塞挂起。可以依靠数据库实现,如行锁、读锁、和写锁等、都是在操作之前加锁,在java中,synchronize的思想也是悲观锁。

final修饰的对象初始化时分两种情况

一,修饰成员对象时有三种初始化方式

1.在定义变量时直接赋值
2.声明完变量后在构造方法中为其赋值
3.声明完变量后在构造代码块中为其赋值

二、修饰类对象有两种方式(静态对象)

1.在定义类变量时直接赋值
2.在静态代码块中赋值

public class TestFinal {

//    一、使用Final修饰符修饰的类的特点:该类不能有子类;
//
//    二、使用Final修饰符修饰的对象的特点:该对象的引用地址不能改变;
//
//    三、使用Final修饰符修饰的方法的特点:该方法不能被重写;
//
//    四、使用Final修饰符修饰的变量的特点:该变量会变成常亮,值不能被改变。

//-----------------成员变量------------------//
//初始化方式一,在定义变量时直接赋值
private final int i = 5;
private final SysUser sysUser = new SysUser();
private final Book book = new Book();

//初始化方式二,声明完变量后在构造方法中为其赋值
//如果采用用这种方式,那么每个构造方法中都要有j赋值的语句
private final int j;
private final Book book1;

public TestFinal() {
    j = 5;
    book1=new Book("三国演义","罗贯中");
//        book=book1;
//        book.setName("111");
//        book.setAuthor("2222");
}

//如果取消该构造方法的注释,程序就会报错,因为它没有为j和book1赋值
/*public TestFinal(String str) {

}*/
public TestFinal(String str) {
    // 为了方便我们可以这样写
    this();
}


//下面的代码同样会报错,因为对j重复赋值
//    public TestFinal(String str1, String str2) {
//        this();
//        j = 3;
//    }


//初始化方式三,声明完变量后在构造代码块中为其赋值
//如果采用此方式,就不能在构造方法中再次为其赋值
//构造代码块中的代码会在构造函数之前执行,如果在构造函数中再次赋值,
//就会造成final变量的重复赋值
private final int k;
private final Book book2;

{
    k = 5;
    book2 = new Book("红楼梦","曹雪芹");
}

//-----------------类变量(静态变量)------------------//
//初始化方式一,在定义类变量时直接赋值
public final static int p = 5;
public final static Book book3 = new Book("水浒传","施耐庵");

//初始化方式二,在静态代码块中赋值
//成员变量可以在构造函数中赋值,但是类变量却不可以。
public final static int q;
public final static Book book4;

static {
    q = 5;
    book4 = new Book("西游记","吴承恩");
}

//因为成员变量属于对象独有,每个对象创建时只会调用一次构造函数,
//因为可以保证该成员变量只被初始化一次;
//而类变量是该类的所有对象共有,每个对象创建时都会对该变量赋值
//这样就会造成变量的重复赋值。

}

HashMap和HashTable的区别

HashTable

1.底层数组+链表实现,无论key还是value都不能为null,线程安全,实现线程安全的方式是在修改数据时锁住整个HashTable,效率低,ConcurrentHashMao做了相关优化.

2.初始size为11,扩容:newsize = olesize*2+1

3.计算index的方法:index = (hash & 0x7FFFFFFF) % tab.length

HashMap

1.底层数组+链表实现,可以存储null键和null值,线程不安全。

2.初始size为16,扩容:newsize = oldsize*2,size一定为2的n次幂

3.扩容针对整个Map,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入

4.插入元素后才判断该不该扩容,有可能无效扩容(插入后如果扩容,如果没有再次插入,就会产生无效扩容)

5.当Map中元素总数超过Entry数组的75%,触发扩容操作,为了减少链表长度,元素分配更均匀‘

6.计算index方法:index = hash & (tab.length – 1)

数组去重

public class Test {

public static void main(String [] args) {
    //数组去重:以下是开发过程中使用较多且较快的2种方法
    String [] arrStr = {"Java", "C++", "Php", "C#", "Python", "C++", "Java"};
    //方法一:
    testA(arrStr);
    System.out.println("----------------------------");
    //方法二:
    testB(arrStr);
}

//方法一:通过list去重
public static void testA(String [] arrStr) {
    List<String> list = new ArrayList<String>();
    for (int i=0; i<arrStr.length; i++) {
        if(!list.contains(arrStr[i])) {
            list.add(arrStr[i]);
        }
    }
    //返回一个包含所有对象的指定类型的数组
    String[] newArrStr =  list.toArray(new String[1]);
    System.out.println(Arrays.toString(newArrStr));
}

//方法二:通过map去重
public static void testB(String [] arrStr) {
    Map<String, Object> map = new HashMap<String, Object>();
    for (String str : arrStr) {
        map.put(str, str);
    }
    //返回一个包含所有对象的指定类型的数组
    String[] newArrStr =  map.keySet().toArray(new String[1]);
    System.out.println(Arrays.toString(newArrStr));
}

}

SQL的一些优化策略

SQL优化的原因

在项目上线初期的时候可能感觉优化和不优化没什么关系,但是随着项目数据的增加,业务的增多,这个时候SQL的效率就会对程序造成影响,此时SQL优化就显得很重要。

SQL优化的一些方法

1.在查询时应尽量避免在where子句中对字段进行null判断

2.尽量避免在where查询中使用!= 或< > 操作符

3.尽量避免在where子句中使用or来连接条件

实例:

select id from t where num = 10 or num = 20

可改为:

select id from t where num = 10

nuion all

select id from t where num = 20

4. in 和 not in也要慎用

实例:

select id from t where num in (1,2,3)

可改为:

select id from t where num between 1 and 4

5.模糊查询也应该慎用 全模糊查询(%…%),左模糊查询(%…),都会导致全表扫描,但是右模糊查询(…%)则不会全表扫描

6.应尽量避免在where字句中对字段进行表达式操作

实例:

select id from t where num/2 = 10

可改为:

select id from t where num = 100*2

7.尽量避免在where字句中对字段进行函数操作

实例:

select id from t where substring (name, 1, 3) = 'abc'

可改为:

select id from t where name like 'abc%'

8.不要在where字段中的“=” 左边进行函数,算数运算或其他表达式运算

总结:以上这些会导致全表扫描

9.在使用索引作为条件时,若有复合索引,那么必须使用该索引中第一个字段作为条件才能保证系统使用该索引,否则该索引不会被使用,并且应尽可能的让字段顺序与索引保存一致

10.很多时候使用exists代替in较好

实例:

select num from a where num in (select num from b)

可改为:

select num from a where exists (select 1 from b where num = a.num)

11.并不是所有的索引对查询都有,SQL是根据表中数据来进行查询优化的,当索引有大量数据重复时,SQL查询不会利用索引。

12.索引并不是越多越好,索引固然可以提高相应select的效率,但同时也降低了insert以及update的效率,因为insert或update可能会重建索引,所以建索引需要慎重考虑,一个表的索引最好不要超过6个

13.尽量使用数字类型字段,若只含数值信息尽量不要设计为字符型,这样会降低性能,并会增加内储开销,因为引擎在处理查询和连接时会逐个比较字符串中的每一个字符,对于数字类型而言只需要比较一次就够了

14.尽量使用varchar代替char,因为varchar储存空间小,可以节省空间,其次对于查询而言,在一个较小的字段内搜索效率显然要高些。

15.不要使用selectfrom t, 这样会全表查询,用具体的字段代替,不要返回用不到的字段

16.应避免频繁创建和删除临时表,以减少内存的消耗

17.临时表并不是不可使用的,适当的使用他们可以使某些线程更有效,例如,当需要重复使用大型表或常用表中的某个数据集时,但是,对于一次性事件最好到处表

18.在兴建临时表时,如果一次性插入数据量很大,name可以使用select into代替create table,避免大量log以提高速度;若数据量不打为了缓和数据表的资源,应该先create table 再 insert

19.如果使用到了临时表,在储存过程的最后务必将所有的临时表格式删除,先truncate table ,然后再drop table 这样可以避免系统表较长时间锁定。

20.尽量避免使用游标,因为游标效率较差,如果游标操作的数据超过1万行,那么可以考虑改写

21.尽量避免大事物操作,提高系统并发能力

22.尽量避免向客户端返回大量数据,若数据量过大,应考虑相应需求是否合理

Stream和迭代器的区别

什么是迭代器?

迭代器是提供一种访问一个集合对象各个元素的途径,同时又不需要暴露该对象的内部细节,java通过提供Iterator和terable两个接口来实现集合类的可迭代性,迭代器主要的用法是:首先hasNext()作为循环条件,再用next()方法得到每一个元素。

public static void main(String[] args) {
	  List<String>list=new ArrayList<>();  
     
       list.add("a");  
     
        list.add("b");  
     
        Iterator<String>it=list.iterator();//得到lits的迭代器  
      
        //调用迭代器的hasNext方法,判断是否有下一个元素  	     
        while (it.hasNext()) {  
          
            //将迭代器的下标移动一位,并得到当前位置的元素值  
            System.out.println(it.next());    
        }     
}

注意:从java5.0开始,迭代器可以被foreach循环所替代,但是foreach循环本质也是使用Iterator进行遍历的

什么是Stream?

Stream不是集合元素,它不是数据结构并不保存数据,它是有关算法和计算的,它更像一个高级版本的Iterator。原始版本的Iterator,用户只能显式的一个一个遍历元素并对其执行某些操作;高级版本的Stram,用户只要给出需要对其包含的元素执行相应的操作

Stream和迭代器的区别

Stream可以并行化操作,迭代器只能命令的、串行化操作。也就是当使用串行方式去遍历时,每个item读完后再读下一个item。而使用并行去遍历时,数据会被分成多个段,其中每一个都在不同的线程中处理,然后将结果一起输出。Stream的并行操作依赖于java7中引入的fork/Join框架来拆分任务和加速处理过程。

使用场景:

当数据量不大或者没有太耗时的操作时,顺序执行往往比并行执行更快。当任务涉及到耗时操作并任务之间不互相依赖时,那么并行化就是一个不错的选择。通常而言,将这类程序并行化之后,执行速度会提升好几个等级

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值