1. Java面向对象的特征 (必会)

面向对象的三个基本特征是:封装、继承、多态。
(1)封装:通常认为封装是把数据和操作数据的方法绑定起来,对数据的访问只能通过已定义的接口。面向对象的本质就是将现实世界描绘成一系列完全自治、封闭的对象。我们在类中编写的方法就是对实现细节的一种封装;我们编写一个类就是对数据和数据操作的封装。可以说,封装就是隐藏一切可隐藏的东西,只向外界提供最简单的编程接口。
(2)继承:继承是从已有类得到继承信息创建新类的过程。提供继承信息的类被称为父类(超类、基类);得到继承信息的类被称为子类(派生类)。继承让变化中的软件系统有了一定的延续性,同时继承也是封装程序中可变因素的重要手段。
(3)多态:多态性是指允许不同子类型的对象对同一消息作出不同的响应。简单的说就是用同样的对象引用调用同样的方法但是做了不同的事情。多态性分为编译时的多态性和运行时的多态性。如果将对象的方法视为对象向外界提供的服务,那么运行时的多态性可以解释为:当 A系统访问B系统提供的服务时,B 系统有多种提供服务的方式,但一切对 A 系统来说都是透明的。方法重载(overload)实现的是编译时的多态性(也称为前绑定),而方法重写(override)实现的是运行时的多态性(也称为后绑定)。运行时的多态是面向对象最精髓的东西,要实现多态需要做两件事:
第一:方法重写(子类继承父类并重写父类中已有的或抽象的方法);
第二:对象造型(用父类型引用指向子类型对象,这样同样的引用调用同样的方法就会根据子类对象的不同而表现出不同的行为)。
(4)抽象:抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。

2. 请你说说“面向对象六大原则”?

单一职责原则(Single-Resposibility Principle)。

“对一个类而言,应该仅有一个引起它变化的原因。”本原则是我们非常熟悉地”高内聚性原则”的引申,但是通过将”职责”极具创意地定义为”变化的原因”,使得本原则极具操作性,尽显大师风范。同时,本原则还揭示了内聚性和耦合生,基本途径就是提高内聚性;如果一个类承担的职责过多,那么这些职责就会相互依赖,一个职责的变化可能会影响另一个职责的履行。其实OOD的实质,就是合理地进行类的职责分配。

开放封闭原则(Open-Closed principle)。

“软件实体应该是可以扩展的,但是不可修改。”本原则紧紧围绕变化展开,变化来临时,如果不必改动软件实体裁的源代码,就能扩充它的行为,那么这个软件实体设计就是满足开放封闭原则的。如果说我们预测到某种变化,或者某种变化发生了,我们应当创建抽象类来隔离以后发生的同类变化。在Java中,这种抽象是指抽象基类或接口;在C++中,这各抽象是指抽象基类或纯抽象基类。当然,没有对所有情况都贴切的模型,我们必须对软件实体应该面对的变化做出选择。

Liskov替换原则(Liskov-Substituion Principle)。

“子类型必须能够替换掉它们的基类型。”本原则和开放封闭原则关系密切,正是子类型的可替换性,才使得使用基类型模块无需修改就可扩充。Liskov替换原则从基于契约的设计演化而来,契约通过为每个方法声明”先验条件”和”后验条件”;定义子类时,必须遵守这些”先验条件”和”后验条件”。当前基于契的设计发展势头正劲,对实现”软件工厂”的”组装生产”梦想是一个有力的支持。

依赖倒置原则(Dependecy-Inversion Principle)。

“抽象不应依赖于细节,细节应该依赖于抽象。”本原则几乎就是软件设计的正本清源之道。因为人解决问题的思考过程是先抽象后具体,从笼统到细节,所以我们先生产出的势必是抽象程度比较高的实体,而后才是更加细节化的实体。于是,”细节依赖于抽象”就意味着后来的依赖于先前的,这是自然而然的重用之道。而且,抽象的实体代表着笼而统之的认识,人们总是比较容易正确认识它们,而且本身也是不易变的,依赖于它们是安全的。依赖倒置原则适应了人类认识过程的规律,是面向对象设计的标志所在。

接口隔离原则(Interface-Segregation Principle)。

“多个专用接口优于一个单一的通用接口。”本原则是单一职责原则用于接口设计的自然结果。一个接口应该保证,实现该接口的实例对象可以只呈现为单一的角色;这样,当某个客户程序的要求发生变化,而迫使接口发生改变时,影响到其他客户程序的可能生性小。

良性依赖原则。

“不会在实际中造成危害的依赖关系,都是良性依赖。”通过分析不难发现,本原则的核心思想是”务实”,很好地揭示了极限编程(Extreme Programming)中”简单设计”各”重构”的理论基础。本原则可以帮助我们抵御”面向对象设计五大原则”以及设计模式的诱惑,以免陷入过度设计(Over-engineering)的尴尬境地,带来不必要的复杂性。

3. 谈谈final、finally、finalize的区别 (必会)

1.final

根据程序上下文环境,Java关键字final有“这是无法改变的”或者“终态的”含义,它可以修饰非抽象类、非抽象类成员方法和变量。你可能出于两种理解而需要阻止改变:设计或效率。

  • final类不能被继承,没有子类,final类中的方法默认是final的。
  • final方法不能被子类的方法覆盖,但可以被继承。
  • final成员变量表示常量,只能被赋值一次,赋值后值不再改变。
  • final不能用于修饰构造方法。

2.finally

finally是关键字,在异常处理中,try子句中执行需要运行的内容,catch子句用于捕获异常,finally子句表示不管是否发生异常,都会执行。finally可有可无。但是try…catch必须成对出现。

3.finalize()

finalize() 方法名,Object类的方法,Java 技术允许使用 finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没有被引用时 , 对这个对象进行调用。finalize()方法是在垃圾收集器删除对象之前对这个对象调用的子类覆盖 finalize() 方法以整理系统资源或者执行其他清理操作。

4. Java 内部类和静态内部类的区别 (必会)

  • 1.静态内部类可以有静态成员(方法,属性),而非静态内部类则不能有静态成员(方法,属性)。
  • 2.静态内部类只能够访问外部类的静态成员,而非静态内部类则可以访问外部类的所有成员(方法,属性)。
  • 3.实例化一个非静态的内部类的方法:
  • a.先生成一个外部类对象实例 OutClassTest oc1 = new OutClassTest();
  • b.通过外部类的对象实例生成内部类对象 OutClassTest.InnerClass nostaticinner = oc1.new InnerClass();
  • 4.实例化一个静态内部类的方法:
  • a.不依赖于外部类的实例,直接实例化内部类对象 OutClassTest.InnerStaticClass inner = new OutClassTest.InnerStaticClass();
  • b.调用内部静态类的方法或静态变量,通过类名直接调用 OutClassTest.InnerStaticClass.static_value OutClassTest.InnerStaticClass.getMessage()
  1. 静态内部类是指被声明为static的内部类,可不依赖外部类实例化;而非静态内部类需要通过生成外部类来间接生成。
  2. 静态内部类只能访问外部类的静态成员变量和静态方法,而非静态内部类由于持有对外部类的引用,可以访问外部类的所用成员

5. Java 中的 ==, equals 与 hashCode 的区别与联系 (必会)

概念:

  • == : 该操作符生成的是一个boolean结果,它计算的是操作数的值之间的关系
  • equals : Object 的 实例方法,比较两个对象的content是否相同
  • hashCode : Object 的 native方法 , 获取对象的哈希值,用于确定该对象在哈希表中的索引位置,它实际上是一个int型整数

关系操作符 ==:

  • 若操作数的类型是基本数据类型,则该关系操作符判断的是左右两边操作数的值是否相等
  • 若操作数的类型是引用数据类型,则该关系操作符判断的是左右两边操作数的内存地址(堆内存)是否相同。也就是说,若此时返回true,则该操作符作用的一定是同一个对象。

1. equals

在初学Java的时候,很多人会说在比较对象的时候,==是比较地址,equals()是比较对象的内容,谁说的?
看看equals()方法在Object类中的定义:

  1. publicboolean equals(Object obj){
  2. //底层实际上还是用了==来进行比较
  3. return(this== obj);
  4. }

这里是比较指针(内存中的地址)

为什么会有equals是比较内容的这种说法?
因为在String、Double等封装类中,已经重写(overriding)了Object类的equals()方法,于是有了另一种计算公式,是进行内容的比较。

比如在String类中:

  1. publicint hashCode(){
  2. int h = hash;
  3. if(h ==0){
  4. char val[]= value;
  5. int len = count;
  6. for(int i =0; i < len; i++){
  7. h =31*h + val[off++];
  8. }
  9. hash = h;
  10. }
  11. return h;
  12. }

2. hashCode

想要知道这个hashcode,首先得知道hash:
hash是一个函数,该函数中的实现就是一种算法,就是通过一系列的算法来得到一个hash值。这个时候,我们就需要知道另一个东西,hash表,通过hash算法得到的hash值就在这张hash表中,也就是说,hash表就是所有的hash值组成的,有很多种hash函数,也就代表着有很多种算法得到hash值

hashcode就是通过hash函数得来的,通俗的说,就是通过某一种算法得到的,hashcode就是在hash表中有对应的位置。

hashcode代表对象的地址说的是对象在hash表中的位置,物理地址说的对象存放在内存中的地址

hashcode有什么作用:
1、HashCode的存在主要是为了查找的快捷性,HashCode是用来在散列存储结构中确定对象的存储地址的(后半句说的用hashcode来代表对象就是在hash表中的位置)

  1. public native int hashCode();

是一个本地方法,返回的对象的地址值。
但是,同样的思路,在String等封装类中对此方法进行了重写。方法调用得到一个计算公式得到的 int值.

3. hashCode和equlas的关系

  • 1、如果两个对象相同(即用equals比较返回true),那么它们的hashCode值一定要相同;
  • 2、如果两个对象的hashCode相同,它们并不一定相同(即用equals比较返回false)
  • 原因:从散列的角度考虑,不同的对象计算哈希码的时候,可能引起冲突,大家一定还记得数据结构中。

由于为了提高程序的效率才实现了hashcode方法,先进行hashcode的比较,如果不同,那没就不必在进行equals的比较了,这样就大大减少了equals比较的 次数,这对比需要比较的数量很大的效率提高是很明显的,一个很好的例子就是在集合中的使用

4. 为什么equals方法重写的话,建议也一起重写hashcode方法?

如果对象的equals方法被重写,那么对象的HashCode方法也尽量重写
比如:有个A类重写了equals方法,但是没有重写hashCode方法,看输出结果,对象a1和对象a2使用equals方法相等,按照上面的hashcode的用法,那么他们两个的hashcode肯定相等,但是这里由于没重写hashcode方法,他们两个hashcode并不一样,所以,我们在重写了equals方法后,尽量也重写了hashcode方法,通过一定的算法,使他们在equals相等时,也会有相同的hashcode值。
重写的作用:
如果 重写(用于需求,比如建立一个Person类,比较相等我只比较其属性身份证相等就可不管其他属性,这时候重写)equals,就得重写hashCode,和其对象相等保持一致。如果不重写,那么调用的Object中的方法一定保持一致。
1. 重写equals()方法就必须重写hashCode()方法主要是针对HashSet和Map集合类型。集合框架只能存入对象(对象的引用(基本类型数据:自动装箱))。
在向HashSet集合中存入一个元素时,HashSet会调用该对象(存入对象)的hashCode()方法来得到该对象的hashCode()值,然后根据该hashCode值决定该对象在HashSet中存储的位置。简单的说:HashSet集合判断两个元素相等的标准是:两个对象通过equals()方法比较相等,并且两个对象的HashCode()方法返回值也相等。如果两个元素通过equals()方法比较返回true,但是它们的hashCode()方法返回值不同,HashSet会把它们存储在不同的位置,依然可以添加成功。同样:在Map集合中,例如其子类Hashtable(jdk1.0错误的命名规范),HashMap,存储的数据是对,key,value都是对象,被封装在Map.Entry,即:每个集合元素都是Map.Entry对象。在Map集合中,判断key相等标准也是:两个key通过equals()方法比较返回true,两个key的hashCode的值也必须相等。判断valude是否相等equal()相等即可。
稍微提一句: (1)两个对象,用==比较比较的是地址,需采用equals方法(可根据需求重写)比较。
(2)重写equals()方法就重写hashCode()方法。
(3)一般相等的对象都规定有相同的hashCode。
hash:散列,Map关联数组,字典
2. 集合类都重写了toString方法。String类重写了equal和hashCode方法,比较的是值。
[

](https://blog.csdn.net/u013679744/article/details/57074669)

5. 优美的讲解equals和==的区别

初步认识equals与==的区别:

  1. ==是判断两个变量或实例是不是指向同一个内存空间,equals是判断两个变量或实例所指向的内存空间的值是不是相同
  2. ==是指对内存地址进行比较 , equals()是对字符串的内容进行比较
  3. ==指引用是否相同, equals()指的是值是否相同

equals与==的区别详解:

== 比较的是变量(栈)内存中存放的对象的(堆)内存地址,用来判断两个对象的地址是否相同,即是否是指相同一个对象。比较的是真正意义上的指针操作。
equals用来比较的是两个对象的内容是否相等,由于所有的类都是继承自java.lang.Object类的,所以适用于所有对象,如果没有对该方法进行覆盖的话,调用的仍然是Object类中的方法,而Object中的equals方法返回的却是==的判断。

String s=”abcd”是一种非常特殊的形式,和new 有本质的区别。
它是java中唯一不需要new 就可以产生对象的途径。以String s=”abcd”;形式赋值在java中叫直接量,它是在常量池中而不是象new一样放在压缩堆中。

这种形式的字符串,在JVM内部发生字符串拘留,即当声明这样的一个字符串后,JVM会在常量池中先查找有没有一个值为”abcd”的对象,如果有,就会把它赋给当前引用.即原来那个引用和现在这个引用指点向了同一对象, 如果没有,则在常量池中新创建一个”abcd”,下一次如果有String s1 = “abcd”;又会将s1指向”abcd”这个对象,即以这形式声明的字符串,只要值相等,任何多个引用都指向同一对象.

而String s = new String(“abcd”);和其它任何对象一样.每调用一次就产生一个对象,只要它们调用。
也可以这么理解: String str = “hello”; 先在内存中找是不是有”hello”这个对象,如果有,就让str指向那个”hello”.

如果内存里没有”hello”,就创建一个新的对象保存”hello”.
String str=new String (“hello”) 就是不管内存里是不是已经有”hello”这个对象,都新建一个对象保存”hello”。

6. java中重载与重写的区别 (必会)

重载: 发生在同一个类中,方法名必须相同,参数类型不同.个数不同.顺序不同, 方法返回值和访问修饰符可以不同,发生在编译时。
重写: 发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父类, 抛出的异常范围小于等于父类, 访问修饰符范围大于等于父类;如果父类方法访问修饰符为 private 则子类就不能 重写该方法。

7. int与Integer的区别 (必会)

int与Integer的基本使用对比

  • Integer是int的包装类;int是基本数据类型;
  • Integer变量必须实例化后才能使用;int变量不需要;
  • Integer实际是对象的引用,指向此new的Integer对象;int是直接存储数据值 ;
  • Integer的默认值是null;int的默认值是0。

延伸比较:
关于Integer和int的比较
1、由于Integer变量实际上是对一个Integer对象的引用,所以两个通过new生成的Integer变量永远是不相等的(因为new生成的是两个对象,其内存地址不同)。

2、Integer变量和int变量比较时,只要两个变量的值是向等的,则结果为true(因为包装类Integer和基本数据类型int比较时,java会自动拆箱为int,然后进行比较,实际上就变为两个int变量的比较)

3、非new生成的Integer变量和new Integer()生成的变量比较时,结果为false。(因为 ①当变量值在-128~127之间时,非new生成的Integer变量指向的是java常量池中的对象,而new Integer()生成的变量指向堆中新建的对象,两者在内存中的地址不同;②当变量值在-128~127之间时,非new生成Integer变量时,java API中最终会按照new Integer(i)进行处理(参考下面第4条),最终两个Interger的地址同样是不相同的)

4、对于两个非new生成的Integer对象,进行比较时,如果两个变量的值在区间-128到127之间,则比较结果为true,如果两个变量的值不在此区间,则比较结果为false

java对于-128到127之间的数,会进行缓存,Integer i = 127时,会将127进行缓存,下次再写Integer j = 127时,就会直接从缓存中取,就不会new了

8. 接口和抽象类的区别是什么? (必会)

  1. 实现:抽象类的子类使用 extends 来继承;接口必须使用 implements 来实现接口。
  2. 构造函数:抽象类可以有构造函数;接口不能有。
  3. main 方法:抽象类可以有 main 方法,并且我们能运行它;接口不能有 main 方 法。
  4. 实现数量:类可以实现很多个接口;但是只能继承一个抽象类。
  5. 访问修饰符:接口中的方法默认使用 public 修饰;抽象类中的方法可以是任意访 问修饰符

9. 什么是单例模式? 有几种? (必会)

单例模式:某个类的实例在 多线程环境下只会被创建一次出来。
单例模式有: 饿汉式单例模式、懒汉式单例模式和双检锁单例模式三种

  1. 饿汉式:线程安全,一开始就初始化。
    image.png

  2. 懒汉式:非线程安全,延迟初始化。
    image.png
    3. 双检锁:线程安全,延迟初始化
    image.png

10. jdk1.8 的新特性 (高薪问)

1 Lambda 表达式 Lambda 允许把函数作为一个方法的参数。

  1. new Thread(() -> System.out.println("abc")).start()

2 方法引用 方法引用允许直接引用已有 Java 类或对象的方法或构造方法。

  1. ArrayList<String> list = new ArrayList();
  2. list.add("a");
  3. list.add("b");
  4. list.add("c");
  5. list.foreach(System.out::pritln)

上例中我们将 System.out::println 方法作为静态方法来引用。

3 函数式接口 有且仅有一个抽象方法的接口叫做函数式接口,函数式接口可以被隐式转换为 Lambda 表达式。
通常函数式接口 上会添加@FunctionalInterface 注解。

4 接口允许定义默认方法和静态方法 从 JDK8 开始,允许接口中存在一个或多个默认非抽象方法和静态方法。

5 Stream API 新添加的 Stream API(java.util.stream)把真正的函数式编程风格引入到 Java 中。
这种风格将要处理的元素集 合看作一种流,流在管道中传输,并且可以在管道的节点上进行处理,比如筛选, 排序,聚合等。

  1. List<Stirng> list = Arrays.asList("abc","","abc","bc","efg","abcd","def","jki");
  2. list.stream() //获取集合的流对象
  3. .filter(string -> !string.isEmpty()) //对数据进行过滤操作 , 过滤掉空字符串
  4. .distinct() //去重
  5. .foreach(a -> System.out.println(a)); //遍历打印最终结果

6 日期/时间类改进 之前的 JDK 自带的日期处理类非常不方便,我们处理的时候经常是使用的第三方 工具包,比如 commons-lang 包等。
不过 JDK8 出现之后这个改观了很多,比如日期时间的创建、比较、调整、 格式化、时间间隔等。
这些类都在 java.time 包下,LocalDate/LocalTime/LocalDateTime。

7 Optional 类 Optional 类是一个可以为 null 的容器对象。
如果值存在则 isPresent()方法会返 回 true,调用 get()方法会返回该对象。

  1. String string = "abc";
  2. Optional<String> optional = Optional.of(string);
  3. boolean present = optional.isPresent();
  4. String value = optional.get();
  5. System.out.println(present+"/"+value);

8 Java8 Base64 实现 Java 8 内置了 Base64 编码的编码器和解码器。

11. Java 的异常(必会)

image.pngThrowable 是所有 Java 程序中错误处理的父类,有两种资类:Error 和 Exception。
Error:表示由 JVM 所侦测到的无法预期的错误,由于这是属于 JVM 层次的严重 错误,导致 JVM 无法继续执行,因此,这是不可捕捉到的,无法采取任何恢复的操作,顶多只能显示错误信息。
Exception:表示可恢复的例外,这是可捕捉到的。
1.运行时异常:都是 RuntimeException 类及其子类异常,如 NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)等, 这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序 逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。运行时异常的特点是 Java 编译器不会检查它,也就是说,当程序中可能出现这类异常,即使没有用 try-catch 语句捕获它,也没有用 throws 子句声明抛出它,也会编译通过。
2.非运行时异常(编译异常):是 RuntimeException 以外的异常,类型上都属于 Exception 类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。 如 IOException、SQLException 等以及用户自定义的 Exception 异常,一般情况下不自定 义检查异常。

常见的 RunTime 异常几种如下:

  1. NullPointerException - 空指针引用异常
  2. ClassCastException - 类型强制转换异常。
  3. IllegalArgumentException - 传递非法参数异常。
  4. ArithmeticException - 算术运算异常
  5. ArrayStoreException - 向数组中存放与声明类型不兼容对象异常
  6. IndexOutOfBoundsException - 下标越界异常
  7. NegativeArraySizeException - 创建一个大小为负数的数组错误异常
  8. NumberFormatException - 数字格式异常
  9. SecurityException - 安全异常
  10. UnsupportedOperationException - 不支持的操作异常

12. Threadloal 的原理(高薪常问)

ThreadLocal:为共享变量在每个线程中创建一个副本,每个线程都可以访问自己 内部的副本变量。
通过 threadlocal 保证线程的安全性。

其实在 ThreadLocal 类中有一个静态内部类 ThreadLocalMap(其类似于 Map), 用键值对的形式存储每一个线程的变量副本,ThreadLocalMap 中元素的 key 为当前 ThreadLocal 对象,而 value 对应线程的变量副本

ThreadLocal 本身并不存储值它只是作为一个 key 保存到 ThreadLocalMap 中,但是这里要注意的是它作为一个 key 用的是弱引用,因为没有强引用链,弱引用在 GC 的时候可能会被回收。这样就会在 ThreadLocalMap 中存在一些 key 为 null 的键值对 (Entry)。因为 key 变成 null 了,我们是没法访问这些 Entry 的,但是这些 Entry 本身是不会被清除的如果没有手动删除对应 key 就会导致这块内存即不会回收也无法访问,也就是内存泄漏。

使用完 ThreadLocal 之后,记得调用 remove 方法。 在不使用线程池的前提下, 即使不调用 remove 方法,线程的”变量副本”也会被 gc 回收,即不会造成内存泄漏的情况。

13. 同步锁、死锁、乐观锁、悲观锁 (高薪常问)

同步锁: 当多个线程同时访问同一个数据时,很容易出现问题。为了避免这种情况出现,我们要保证线程同步互斥 , 就是指并发执行的多个线程,在同一时间内只允许一个线程访问共享数 据。Java 中可以使用 synchronized 关键字来取得一个对象的同步锁。

死锁: 何为死锁,就是多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放

乐观锁: 总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是 在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和 CAS 算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类 似于 write_conditio 机制,其实都是提供的乐观锁。
在 Java 中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。

悲观锁: 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时 候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使 用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了 很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java 中 synchronized 和 ReentrantLock 等独占锁就是悲观锁思想的实现。

14. 说一下 synchronized 底层实现原理?(高薪常问)

synchronized 可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进 入到临界区,同时它还可以保证共享变量的内存可见性。

Java 中每一个对象都可以作为锁,这是 synchronized 实现同步的基础:
• 普通同步方法,锁是当前实例对象
• 静态同步方法,锁是当前类的 class 对象
• 同步方法块,锁是括号里面的对象

15. synchronized 和 volatile 的区别是什么?(高薪常问)

  1. volatile 本质是在告诉 jvm 当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
  2. volatile 仅能使用在变量级别;synchronized 则可以使用在变量、方法、和类级别的。
  3. volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证 变量的修改可见性和原子性。
  4. volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
  5. volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化。

16. synchronized 和 Lock 有什么区别? (高薪常问)

  1. 首先 synchronized 是 java 内置关键字,在 jvm 层面,Lock 是个 java 类;
  2. synchronized 无法判断是否获取锁的状态,Lock 可以判断是否获取到锁;
  3. synchronized 会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发 生异常会释放锁),Lock 需 在 finally 中手工释放锁(unlock()方法释放锁),否则容易造成 线程死锁;
  4. 用 synchronized 关键字的两个线程 1 和线程 2,如果当前线程 1 获得锁,线程 2 线程 等待。如果线程 1 阻塞, 线程 2 则会一直等待下去,而 Lock 锁就不一定会等待下去,如果 尝试获取不到锁,线程可以不用一直等待就结 束了;
  5. synchronized 的锁可重入、不可中断、非公平,而 Lock 锁可重入、可判断、可公平 (两者皆可);
  6. Lock 锁适合大量同步的代码的同步问题,synchronized 锁适合代码少量的同步问题。

17. 手写冒泡排序?(必会)

  1. public class Sort{
  2. //第一种
  3. public static void sort(){
  4. //定义一个数组
  5. int[] arr={1,23,2,6,67,88,99,102,8,10};
  6. int n =arr,length-1;
  7. for(int j=0 ; j<n ; j++){
  8. int last = 0;
  9. for(int i = j; i<arr.length-1-j ; i++){
  10. if(arr[i]<arr[i+1]){
  11. int m = arr[i];
  12. arr[i]=arr[i+1];
  13. arr[i+1]=m;
  14. last=i;
  15. }
  16. n=last;
  17. if(n==0){
  18. break;
  19. }
  20. }
  21. }
  22. }
  23. //第二种
  24. private static void orderNum(int[] arr) {
  25. //定义一个变量
  26. int n = arr.length - 1;
  27. while (true) {
  28. int last = 0;
  29. for (int i = 0; i < n; i++) {
  30. if (arr[i] > arr[i + 1]) {
  31. int m = arr[i];
  32. arr[i] = arr[i + 1];
  33. arr[i + 1] = m;
  34. last = i;
  35. }
  36. }
  37. n = last;
  38. //判断结果是否为0
  39. if (n==0) {
  40. break;
  41. }
  42. }
  43. }
  44. public static void main(String[] args){
  45. //第一种
  46. sort();
  47. //第二种
  48. int[] array = {1, 17, 3, 21, 43, 50, 22, 78, 44, 99, 121};
  49. orderNum(array);
  50. System.out.println(Arrays.toString(array));
  51. }
  52. }

18. String、StringBuffer、StringBuilder 三者之间的区别

String 字符串常量
StringBuffer 字符串变量(线程安全)
StringBuilder 字符串变量(非线程安全)

  1. String 中的 String 类中使用 final 关键字修饰字符数组来保存字符串,private final char value[] ,String 对象是不可变的,也就可以理解为常量,线程安全。

  2. AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了 一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。

  3. StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全 的。

  4. StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。
  • 如果要操作少量的数据用 String;
  • 多线程操作字符串缓冲区下操作大量数据用 StringBuffer;
  • 单线程操作字符串缓冲区下操作大量数据用 StringBuilder。

19. string 常用的方法有哪些?

  1. indexOf():返回指定字符的索引。
  2. charAt():返回指定索引处的字符。
  3. replace():字符串替换。
  4. trim():去除字符串两端空白。
  5. split():分割字符串,返回一个分割后的字符串数组。
  6. getBytes():返回字符串的 byte 类型数组
  7. length():返回字符串长度。
  8. toLowerCase():将字符串转成小写字母。
  9. toUpperCase():将字符串转成大写字符。
  10. substring():截取字符串。
  11. equals():字符串比较。

20. 反射

在 Java 中的反射机制是指在运行状态中,对于任意一个类都能够知道这个类所有 的 属性和方法;并且对于任意一个对象,都能够调用它的任意一个方法;这种动态获 取信息 以及动态调用对象方法的功能成为 Java 语言的反射机制。

获取 Class 对象的 3 种方法 :
调用某个对象的 getClass()方法 Person p=new Person(); Class clazz=p.getClass();
调用某个类的 class 属性来获取该类对应的 Class 对象 Class clazz=Person.class;
使用 Class 类中的 forName()静态方法(最安全/性能最好) Class clazz=Class.forName(“类的全路径”); (最常用)

21. BIO、NIO、AIO 有什么区别?(高薪常问)

  1. BIO:Block IO 同步阻塞式 IO,就是我们平常使用的传统 IO,它的特点是模式简 单使用方便,并发处理能力低。
  2. NIO:New IO 同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端通过 Channel(通道)通讯,实现了多路复用。
  3. AIO:Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了异步非堵塞 IO , 异步 IO 的操作基于事件和回调机制。

22. IO和NIO的区别

NIO和IO的主要区别
下表总结了Java IO和NIO之间的主要区别

IO NIO
面向流 面向缓冲
阻塞IO 非阻塞IO
选择器

1、面向流与面向缓冲
Java IO和NIO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。 Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。
2、阻塞与非阻塞IO
Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。
3、选择器(Selectors)
Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。