JAVA-基础笔记

java的特点

  1. java是一门面向对象的编程语言
    1. 一次编写,到处运行,实现这种特性的真实java虚拟机,已编译的java程序可以在任何带有JVM平台上运行
  2. 具有平台的独立性和移植性
  3. 具有稳健性
    1. java是一个强类型语言,它允许扩展编译时检查潜在的类型不匹配问题。java要求显式的方法声明,它不支持C风格的隐式声明。这些严格的要求保证编译程序时能捕捉调用错误
    2. 异常处理是java中使得程序更稳健的另一个特征。异常是某种类似于错误的异常条件出现信号,使用try/catch/finally语句,开发者可以找出错误的处理代码程序,这就简化了出错处理和恢复的任务

java是如何实现跨平台的

  1. java是通过JVM虚拟机实现跨平台的
  2. JVM可以理解成一个软件,不同的平台有不同的版本,我们编写的java代码,编译后会生成一个class文件(字节码文件)。java虚拟机就是将字节码文件翻译成特定平台下的机器码,通过JVM翻译成机器码之后才能运行。不同平台下生成的字节码是一样的,但由于JVM翻译的机器码却不一样,只要在不同平台上安装对应的JVM,就可以运行字节码文件,运行我们的程序,因此java的运行必须有JVM的支持,因为编译的结果不是机器码,必须要经过JVM的翻译才能运行。

java和c++的区别

  1. java是面向对象的编程语言,所有的对象都继承自java.lang.Object,C++兼容C。不但支持面向对象也支持面向过程
  2. java通过虚拟机实现跨平台,C++依赖与特定平台
  3. java没有指针,它引用可以理解为安全指针,而C++具有和C一样的指针
  4. java支持自动垃圾回收,而C++需要手动回收
  5. java不支持多继承,只能通过实现多个接口来达到相同的目的,而C++至此多继承

JDK/JRE/JVM三者之间的关系

  1. JVM,
    1. 英文名称是:java Virtual Machine,java能实现跨平台运行的核心在于JVM,所有的java程序都会首先编译成class的类文件,这种类文件可以在虚拟机上执行,也就是说class文件并不直接与机器的操作系统交互,而是通过虚拟机间接与操作系统交互,由虚拟机将程序解释给本地系统。 针对不同的系统有不同的 jvm 实现,有 Linux 版本的 jvm 实现,也有Windows 版本的 jvm 实现,但是同一段代码在编译后的字节码是一样的。这就是Java能够跨平台,实现一次编写,多处运行的原因所在。

20220402230447.png

  1. JRE
    1. 英文名称(Java Runtime Environment),就是Java 运行时环境。我们编写的Java程序必须要在JRE才能运行。它主要包含两个部分,JVM 和 Java 核心类库。

image.png
JRE是Java的运行环境,并不是一个开发环境,所以没有包含任何开发工具,如编译器和调试器等。
如果你只是想运行Java程序,而不是开发Java程序的话,那么你只需要安装JRE即可。

  1. JDK
    1. 英文名称(Java Development Kit),就是 Java 开发工具包学过Java的同学,都应该安装过JDK。当我们安装完JDK之后,目录结构是这样的

image.png
可以看到,JDK目录下有个JRE,也就是JDK中已经集成了 JRE,不用单独安装JRE。
另外,JDK中还有一些好用的工具,如jinfo,jps,jstack等。
image.png

  1. 最后总结一下三者之间的关系
    1. JRE = JVM + Java 核心类库
    2. JDK = JRE + Java工具 + 编译器 + 调试器image.png

java的程序时编译执行还是解释执行

  1. 什么是编译型语言和解释型语言
  2. 编译型语言
    1. 在程序运行之前,通过编译器将源程序编译成机器码可运行的二进制,以后执行这个程序时,就不用进行编译了
    2. 优点:编译器一般会有预编译的过程对代码进行优化,因为只编译一次,运行时不需要编译,所以编译型语言的程序执行效率高,可以脱离环境独立运行
    3. 缺点:编译之后如果需要修改就需要整个模块重新编译。编译时根据对应的环境生成机器码,不同的操作系统之间移植就会存在问题,需要根据运行的操作系统环境编译成不同的可执行文件
    4. 总结:执行速度快,效率高,依赖编译器,跨平台性偏弱、
    5. 代表语言:C、C++、Pascal、Object-C以及Swift。
  3. 解释型语言
    1. 定义:解释型语言的源代码不是直接翻译成机器码,而是先翻译成中间代码,再由解释器对中间代码进行解释运行。在运行时才将源程序代码翻译成机器码,翻译一句,执行一句,直到结束
    2. 优点:
      1. 拥有良好的平台兼容性,在任何环境中都可以运行,前提是安装了解释器
      2. 灵活,修改代码时,可以直接修改快速部署,不用停机维护
    3. 缺点:
      1. 每次运行时都需要解释一边,性能上不如编译型语言,
      2. 总结:解释型语言执行速度慢,效率低,依赖解释器,跨平台性好
      3. 代表语言:JavaScript、Python、Erlang、PHP、Perl、Ruby。
  4. java语言,它的源代码会先通过javac编译成字节码,再通过JVM将字节码装欢成机器码执行,即解释运行和编译运行配合使用,所以可以称为混合型或者半编译型语言

面向对象和面向过程的区别

  1. 面向对象和面向过程是一种软件开发思想
  2. 面向过程就是分析出解决问题所需步骤,然后用函数按这些步骤实现,使用时一次调用即可
  3. 面向对象是把问题构成问题事物分解成各个对象,分别设计这些对象,然后将他们组装成有完整功能的系统,面向对象是用类实现各个功能模块,面向过程只用函数实现
  4. 以五子棋为例,面向过程的设计思路就是首先分析问题的步骤:
    1. 1、开始游戏,
    2. 2、黑子先走,
    3. 3、绘制画面,
    4. 4、判断输赢,
    5. 5、轮到白子,
    6. 6、绘制画面,
    7. 7、判断输赢,
    8. 8、返回步骤2,
    9. 9、输出最后结果。
    10. 把上面每个步骤用分别的函数来实现,问题就解决了。
    11. 而面向对象的设计则是从另外的思路来解决问题。整个五子棋可以分为:黑白双方棋盘系统,负责绘制画面规则系统,负责判定诸如犯规、输赢等。黑白双方负责接受用户的输入,并告知棋盘系统棋子布局发生变化,棋盘系统接收到了棋子的变化的信息就负责在屏幕上面显示出这种变化,同时利用规则系统来对棋局进行判定。

面向对象有哪些特性

  1. 面向对象四大特性:封装,继承,多态,抽象
  2. 封装
    1. 就是将类的信息隐藏在类的内部,不允许外部程序直接访问,而是通过该类的实现方法实现对隐藏信息的操作和访问。良好的封装能够减少耦合
  3. 继承
    1. 从已有的类中派生出新的类,新的类继承父类的属性和行为,并能够在父类的基础上进行扩展,大大增强了程序的重用性和易维护性,在java中是单继承的,也就是说一个子类只能有一个父类
  4. 多态
    1. 是同一个行为具有不同表现形式的能力,在不修改程序代码的情况下改变程序运行时绑定的代码。实现多态的三要素是:继承、重写、父类引用指向子类对象
    2. 静态多态:通过重载实现,相同的方法拥有不同的参数,可以根据参数的不同,做出不同的处理
    3. 动态多态:在子类中重写父类的方法,运行期间判断所引用的对象的具体类型,根据实际类型调用相应的方法
  5. 抽象
    1. 把客观事物用代码抽象出来

面向对象编程的六大原则

  1. 完整版
    1. 对象单一原则:设计创建的对象,必须职责明确,比如商品类,里面相关的属性和方法必须和商品相关,不能出现订单等不相关的内容。这里的类可以是模块,类库,程序集,而不是单单指的是类
    2. 里氏替换原则:子类能够完全替换父类,反之则不行。通常用于实现接口时运用。因为子类能够完全替代父类,那么这样父类就拥有很多子类,在后续的程序扩展中就很容易进行扩展,程序完全不需要进行修改就可以扩展。比如IA的实现方法为A,因为在后续的变更中,需要新的实现,直接在容器注入出更换接口即可
    3. 迪米特法则:也叫最小原则,或者说最小耦合。通常在设计程序或开发程序时,尽量高内聚低耦合。当两个类进行交互时,会产生依赖。而迪米特法则就是建议这种依赖越少越好。就像构造函数注入父类对象一样,但需要依赖某个对象时,并不在意其内部时如何实现的,而是在容器中注入相关的实现,即符合啊里氏替换原则,又起到了解耦的作用。
    4. 开闭原则:开放扩展,封闭修改。当项目需求发生变更时,尽量的不去对原有代码进行修改,而是在原有基础上进行扩展。
    5. 依赖倒置原则:高层模块不应该直接依赖与底层模块的具体实现,而应该依赖于底层的抽象。接口和抽象类不应该依赖于实现类,而实现类依赖接口或抽象类。
    6. 接口隔离原则:一个对象和另外一个对象交互过程中,依赖的内容最小。也就是说在接口设计时,在遵循对象单一职责情况下,尽量减少接口的内容。
  2. 简洁版
    1. 单一职责:对象设计 要求独立,不能设计万能对象
    2. 开闭原则:对象修改最小化
    3. 里氏替换:程序扩展中抽象被具体,可以替换(接口、父类、可以被实现类对象、子类替换对象)
    4. 迪米特法则:高内聚低耦合。尽量不依赖细节
    5. 依赖倒置:面向抽象编程。也就是参数传递,或者返回值,可以使用父类类型或者接口类型。从广义上讲:基于接口编程,提前设计好接口框架。
    6. 接口隔离:接口设计大小适中。过大导致污染,过小导致调用麻烦

数组到底是不是对象

  1. 对象概念回顾:对象是根据某个类创建出来的一个实例,表示某类事物中一个具体的个体。对象具有各种属性,并且具有一些特定行为。在计算机角度,对象就是内存中的一个内存块,这个内存块封装了一些数据,也就是类中定义的各个属性。所以对象是用来封装数据的。
  2. java中的数组具有java中其他对象的一些基本特点。比如封装了一些数据,可以访问属性也可以调用方法,因此可以说数组是对象。以下输出结果为Object:由此可以看出数组类的父类就是Object类,那么可以推断出数组就是对象。
 Class<Integer[]> aClass = Integer[].class;
System.out.println(aClass.getSuperclass().getName());

java的基本数据类型有哪些

  1. byte ------------>8bit
  2. char------------->16bit
  3. short ----------->16bit
  4. int--------------->32bit
  5. float------------->32bit
  6. long------------->64bit
  7. double---------->64bit
  8. boolean 只有两个值:true、false,可以使用1bit来存储。在Java规范中,没有明确给出boolean的大小。在《java虚拟机规范》给出了单个boolean占4个字节,boolean数组占一个字节的定义,具体还要看虚拟机实现是否按照规范来,因此boolean占用一个字节或者4个字节都是有可能的。
    | 简单类型 | boolean | byte | char | short | Int | long | float | double |
    | — | — | — | — | — | — | — | — | — |
    | 二进制位 | 1 | 8 | 16 | 16 | 32 | 64 | 32 | 64 |
    | 包装类 | Boolean | Byte | Character | Short | Integer | Long | Float | Double |

为什么不能使用浮点型表示金额

  1. 由于计算机中保存的小数其实是十进制的小数近似值,并不是准确值,所以千万不要在代码中使用浮点数来表示金额等重要指标,建议使用Bigdecima或者Long来表示金额。

什么是值传递和引用传递

  1. 值传递是对进本变量而言的,传递的是该变量的一个副本,改变副本不影响原变量
  2. 引用传递一般是对于对象型变量而言,传递的是该对象地址的一个副本,并不是原对象本身,两者指向同一片内存空间。所以引用对象进行操作会同时改变原对象。
  3. java中不存在引用传递,只有值传递。即不存在变量A指向变量B,变量B指向对象的这种情况。

了解java的包装类型吗?为什么需要包装类

  1. java是面向对象的语言,很多地方需要使用对象而不是基本数据数据类型。比如在集合类中,我们是无法将int、double等类型放进去的。因为集合的容器要求元素必须是Object类型。为了让基本类型也拥有对象的特征,就出现了包装类型。相当于讲基本数类型包装起来,使得它具有了对象的性质,并且为其添加了属性和方法,丰富了基本类型的操作。

自动装箱和拆箱

  1. 装箱:将基础类型转化为包装类型
  2. 拆箱:将包装类型转化为基础类型
  3. 当基础类型与他们的包装类有如下几种情况时,编译器会自动帮我们进行装箱或拆箱:
    1. 赋值操作(拆箱或装箱)
    2. 进行加减乘除混合运算(拆箱)
    3. 进行>,<,==比较运算(拆箱)
    4. 调用equals进行比较(装箱)
    5. ArrayList,HashMap等集合类添加基础类型数据(装箱)
    6. 示例代码:
Integer x = 1;//装箱,调用了Integer.valueof(1)
int y = x;//拆箱,调用了x.intValue()

两个Integer用 == 比较不相等的原因

  1. 常见面试题示例
Integer a = 100;
Integer b = 100;
System.out.println(a == b);

Integer c = 200;
Integer d = 200;
System.out.println(c == d);

  1. 输出
true
false

  1. 为什么第二个输出的是false,Integer源码如下:
public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

  1. Integer c = 200;会调用Integer.valueof(200);而从Integer的valueOf()源码可以看出,这里的实现并不是简单的new Integer,而是用IntegerCache做一个cache.
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;
        String integerCacheHighPropValue =
            sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
        if (integerCacheHighPropValue != null) {
            try {
                int i = parseInt(integerCacheHighPropValue);
                i = Math.max(i, 127);
                // Maximum array size is Integer.MAX_VALUE
                h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
            } catch( NumberFormatException nfe) {
                // If the property cannot be parsed into an int, ignore it.
            }
        }
        high = h;
    }
    ...
}

  1. 这是IntegerCache静态代码块中的一段,默认Integer cache的下限是-128,,上限默认127.当赋值100给Integer时,刚好在这个范围内,所以从cache中取得对应的Interger并返回,所以a和b返回的是同一个对象,所以比较是相等的,当赋值200给Integer时,不在cache的范围内,所以会new Integer并返回,当然比较的结果时不相等的。

String为什么不可变

  1. 如果一个对象在它创建完成之后,不能改变它的状态,那么这个对象是不可变的。不能改变状态的意思是,不能改变对象内的成员变量,包括基本数据类型的值不能改变,引用类型的的变量不能指向其他对象,引用类型指向的对象状态也不能改变。然后来看看在java8中String的源码实现:
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0
}

  1. 从源码可以看出,String对象其实在内部就是一个个字段,存储在这个value数组中。value数组使用fianl修饰,final修饰的变量值不能被修改。因此value不可以指向其他对象。
  2. String类内部所有字段都是私有的,被private修饰,而且String没有对外提供修改内部状态的方法,因此value数组不能被改变。所以String是不能被改变的。
  3. 为什么Strng要设计成不可变?存在以下几个原因
    1. 线程安全:同一个字符串实例可以被多个线程共享,因为字符串不可变,本身就是线程安全的
    2. 支持hash映射和缓存:因为String的hash值经常会被用到,比如作为Map的键,不可变的特性使得hash值也不会变,也不需要重新计算
    3. 出于安全考虑:网络地址URL、文件路径PATH,密码等通常情况下都是以String类型保存的,假若String不是固定不变的,将会引起各种安全隐患。比如将密码用String类型保存,那么它将一直留在内存中,直到垃圾收集器把它清除。假如String类不是固定不变的,那么这个密码可能会被改变,导致出现安全隐患。
    4. 字符串常量池优化:String对象创建之后,会缓存到字符串常量池中,下次需要创建同样的对象时,可以直接返回缓存的引用。
  4. 既然String是不可变的,它内部还有很多,substring,replace,replaceAll这些操作的方法,这些方法好像会改变String对象。如何解释?
    1. 每次调用replace等方法,其实在堆内存中创建了一个新的对象,然后其value数组引用指向不同的对象。

为何JDK9要将String的底层实现由char[]改成byte[]

  1. 主要就是为了节约String占用的内存
  2. 在大部分的java程序的堆内存中,String占用的空间最大,并且绝大多数String只有Latin-1字符,这些Latin-1只需一个字节就够了。而在JDK9之前,JVM因为String使用char数组存储,每个char占2个字节,所以即使字符串只需要一个字节,它也要按照2个字节分配,浪费了一半的内存空间。到了JDK9之后,对于每个字符串,会先判断它是不是之后Latin-1字符,如果是,就按照1字节的规格进行内存分配,如果不是就按照2字节的规格进行内存分配,遮掩提高了内存的使用率,同时GC的次数也会减少,提升效率。
  3. 不过Latin-1编码集支持的字符有限,比如不支持中文字符,因此对于中文字符串,用的是UTF16编码(两个字节),所以用byte[]和char[]实现没什么区别。

String,StringBuffer,StringBuilder区别

  1. 可变性
    1. String不可变
    2. StringBuffer和StringBuilder可变
  2. 线程安全
    1. String不可变,因此是线程安全的
    2. StringBuilder不是线程安全的
    3. StringBuffer是线程安全的,内部使用synchronized关键字进行同步

什么是StringJoiner

  1. StringJoiner是java8中新增的一个API,它基于StringBuilder实现,用于实现对字符串之间通过分隔符拼接的场景
  2. StringJoiner有两个构造方法,第一个构造要求依次传入分隔符,前缀和后缀。第二个构造则只要求传入分隔符即可(前缀和后缀默认为空字符串)
StringJoiner(CharSequence delimiter, CharSequence prefix, CharSequence suffix)
StringJoiner(CharSequence delimiter)
  1. 有些字符串拼接的场景,使用StringBuilder和StringBuffer比较繁琐 ,比如以下例子:
List<Integer> values = Arrays.asList(1, 3, 5);
StringBuilder sb = new StringBuilder("(");

for (int i = 0; i < values.size(); i++) {
	sb.append(values.get(i));
	if (i != values.size() -1) {
		sb.append(",");
	}
}

sb.append(")");

而通过StringJoiner来实现拼接List的各个元素,代码看起来更加简洁

List<Integer> values = Arrays.asList(1, 3, 5);
StringJoiner sj = new StringJoiner(",", "(", ")");

for (Integer value : values) {
	sj.add(value.toString());
}

另外,像平时经常使用的Collectors.joining(“,”),底层就是通过StringJoiner实现的。
源码如下:

public static Collector<CharSequence, ?, String> joining(
    CharSequence delimiter,CharSequence prefix,CharSequence suffix) {
    return new CollectorImpl<>(
            () -> new StringJoiner(delimiter, prefix, suffix),
            StringJoiner::add, StringJoiner::merge,
            StringJoiner::toString, CH_NOID);
}

String类的常用方法有哪些

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

new String(“Mylucy”)会创建几个对象

  1. 使用这种方式会创建两个字符串对象(前提是在字符串常量池中没有"Mylucy"这个字符串对象)
  2. "Mylucy"属于字符串字面量,因此在编译时期会在字符串常量池中创建一个字符串对象
  3. 使用new的方式会在堆中创建一个字符串对象

什么是字符串常量池

  1. 字符串常量池(String Pool)保存着所有字符串字面量,这些字面量在编译时期就确定,字符串常量池位于堆内存中,专门用来存储字符串常量。在创建字符串时,JVM首先会检查字符串常量池,如果字符串已经存在池中,则返回其引用,如果不存在,则创建此字符串并放入池中,并返回其引用。

String的最大长度是多少

  1. String类提供了一个length方法,返回值为int类型,而int的取值上限为231-1,所以理论上String的最大长度231-1

Object常用方法有哪些

  1. 常用方法有: toString()、equals()、hashCode()、clone等;
  2. toString:默认输出对象地址
public class Person {
    private int age;
    private String name;

    public Person(int age, String name) {
        this.age = age;
        this.name = name;
    }

    public static void main(String[] args) {
        System.out.println(new Person(18, "Mylucy").toString());
    }
    //output
    //me.tyson.java.core.Person@4554617c
}

可以重写toString方法,按照重写逻辑输出对象值

public class Person {
    private int age;
    private String name;

    public Person(int age, String name) {
        this.age = age;
        this.name = name;
    }

    @Override
    public String toString() {
        return name + ":" + age;
    }

    public static void main(String[] args) {
        System.out.println(new Person(18, "Mylucy").toString());
    }
    //output
    //程序员大彬:18
}

  1. equlas:默认比较两个引用变量是否指向同一个对象(内存地址)
public class Person {
    private int age;
    private String name;

    public Person(int age, String name) {
       this.age = age;
       this.name = name;
    }

    public static void main(String[] args) {
        String name = "Mylucy";
        Person p1 = new Person(18, name);
        Person p2 = new Person(18, name);

        System.out.println(p1.equals(p2));
    }
    //output
    //false
}

可以重写equlas方法,按照age和name是否相等来判断

public class Person {
    private int age;
    private String name;

    public Person(int age, String name) {
        this.age = age;
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (o instanceof Person) {
            Person p = (Person) o;
            return age == p.age && name.equals(p.name);
        }
        return false;
    }

    public static void main(String[] args) {
        String name = "Mylucy";
        Person p1 = new Person(18, name);
        Person p2 = new Person(18, name);

        System.out.println(p1.equals(p2));
    }
    //output
    //true
}

  1. hashCode:将与对象相关的信息映射成一个哈希值,默认实现的hashCode值是根据内存地址算出来的
public class Cat {
    public static void main(String[] args) {
        System.out.println(new Cat().hashCode());
    }
    //out
    //1349277854
}

  1. clone:java赋值是赋值对象引用,如果想得到一个对象的副本,使用赋值操作是无法达到目的的。Object对象有个colne();方法,实现了对象中各个属性的复制,但它的可见范围是protected。
protected native Object clone() throws CloneNotSupportedException;
  1. 所以实体类实现克隆的前提是:
    1. 实现Cloneable接口,这是一个标记接口,自身没有方法,这是一种约定。调用clone方法时,会判断有没有实现Cloneable接口,没有实现的话会抛出异常CloneNotSupportedExcption
    2. 覆盖clone()方法,可见性提升为public
public class Cat implements Cloneable {
    private String name;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    public static void main(String[] args) throws CloneNotSupportedException {
        Cat c = new Cat();
        c.name = "Mylucy";
        Cat cloneCat = (Cat) c.clone();
        c.name = "Mylucy-Pure";
        System.out.println(cloneCat.name);
    }
    //output
    //Mylucy
}

  1. getClass:返回此Object的运行时类,常用于java反射机制
public class Person {
    private String name;

    public Person(String name) {
        this.name = name;
    }

    public static void main(String[] args) {
        Person p = new Person("程序员Mylucy");
        Class clz = p.getClass();
        System.out.println(clz);
        //获取类名
        System.out.println(clz.getName());
    }
    /**
     * class com.tyson.basic.Person
     * com.tyson.basic.Person
     */
}

  1. wait
    1. 当前线程调用对象的wait();方法之后,当前线程会释放对象锁进入等待状态,等待其他线程调用此对象的notify()/notifyAll()唤醒或者等待超时时间wait(long timetOut)自动唤醒。线程需要获取obj对象锁上之后才能调用obj.wait()。
  2. notify
    1. obj.notify唤醒在此对象上等待的单个线程,选择时任意性的,notifyAll();唤醒在此对象上等待的所有线程

浅拷贝和深拷贝

  1. 浅拷贝拷贝对象和原始对象的引用类型引用同一个对象
    1. 以下例子,Cat对象里面有个Person对象,调用clone之后,克隆对象和原对象的Person引用的时同一个对象,这就是浅拷贝。
public class Cat implements Cloneable {
    private String name;
    private Person owner;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    public static void main(String[] args) throws CloneNotSupportedException {
        Cat c = new Cat();
        Person p = new Person(18, "程序员Mylucy");
        c.owner = p;

        Cat cloneCat = (Cat) c.clone();
        p.setName("Mylucy");
        System.out.println(cloneCat.owner.getName());
    }
    //output
    //Mylucy
}

  1. 深拷贝:拷贝对象和原始对象的引用类型引用不同对象。以下例子,在clone函数中不仅调用了supper.clone,而且调用了Person对象的clone方法(Person也要实现Cloneable接口并重写clone方法),从而实现了深拷贝。可以看到拷贝对象的值不会受到原对象的影响
public class Cat implements Cloneable {
    private String name;
    private Person owner;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Cat c = null;
        c = (Cat) super.clone();
        c.owner = (Person) owner.clone();//拷贝Person对象
        return c;
    }

    public static void main(String[] args) throws CloneNotSupportedException {
        Cat c = new Cat();
        Person p = new Person(18, "程序员Mylucy");
        c.owner = p;

        Cat cloneCat = (Cat) c.clone();
        p.setName("Mylucy");
        System.out.println(cloneCat.owner.getName());
    }
    //output
    //程序员Mylucy
}

两个对象的hashCode()相同,则equals()是否也一定为true

  1. equlas与hashCode的关系
  2. 如果两个对象调用equlas比较返回true,那么他们的hashCode值一定相同
  3. 如果两个对象的hashCode相同,那么他们并不一定相同
  4. hashCode方法主要是用来提升对象比较的效率,先进行hashCode值比较,如果不相同那么就没必要进行equals比较,这样就大大减少了equals比较次数

为什么重写equals()时一定要重写hashCode()

  1. 之所以重写equals(),重写hashCode(),是为了保证equals()方法返回true的情况下hashCode值也要一致,如果重写了equals没有重写hashCode,就会出现两个对象相等,但是hashCode不相等的情况。这样的情况出现时,当用其中一个的对象作为键保存在hashMap、hashTable或hashSet中,再以另外一个对象作为键值去查找时,就会存在查询不到的情况。

java创建对象有哪几种方式

  1. 使用new语句创建对象
  2. 使用反射,使用Class.newInstance创建对象
  3. 调用clone方法
  4. 运用序列化手段,调用java.io.ObjectInputStream对象的readObject方法

类实例化的顺序

  1. 静态属性,静态代码块
  2. 普通属性,普通代码块
  3. 构造方法
public class LifeCycle {
    // 静态属性
    private static String staticField = getStaticField();

    // 静态代码块
    static {
        System.out.println(staticField);
        System.out.println("静态代码块初始化");
    }

    // 普通属性
    private String field = getField();

    // 普通代码块
    {
        System.out.println(field);
        System.out.println("普通代码块初始化");
    }

    // 构造方法
    public LifeCycle() {
        System.out.println("构造方法初始化");
    }

    // 静态方法
    public static String getStaticField() {
        String statiFiled = "静态属性初始化";
        return statiFiled;
    }

    // 普通方法
    public String getField() {
        String filed = "普通属性初始化";
        return filed;
    }

    public static void main(String[] argc) {
        new LifeCycle();
    }

    /**
     *      静态属性初始化
     *      静态代码块初始化
     *      普通属性初始化
     *      普通代码块初始化
     *      构造方法初始化
     */
}

equals和==的区别

  1. 对于基本数据类型,==比较的是值。基本数据类型没有equals方法
  2. 对于复合数据类型,==比较的是他们的内存地址(是否同意俄国对象)。equlas默认比较地址值,重写的话按照重写的逻辑比较

常见的关键字有哪些

  1. static:可以修饰类的成员方法,类的成员变量
    1. static变量也可称之为静态变量,静态变量和非静态变量的区别是:
      1. 静态变量被所有的对象共享,内存中只存在一个副本,它在当且进当类初始加载时会被初始化
      2. 非静态变量是对象所拥有的,在创建对象时初始化,存在多个内存副本,各个对象拥有的副本互不影响
      3. 以下例子,age为非静态变量,则p1打印结果是:Name:Mylucy,Age:10;若age使用static修饰,则p1打印结果是:Name:Mylucy,Age:12;,因为staic变量在内存中有且仅有一个副本
public class Person {
    String name;
    int age;
    
    public String toString() {
        return "Name:" + name + ", Age:" + age;
    }
    
    public static void main(String[] args) {
        Person p1 = new Person();
        p1.name = "zhangsan";
        p1.age = 10;
        Person p2 = new Person();
        p2.name = "lisi";
        p2.age = 12;
        System.out.println(p1);
        System.out.println(p2);
    }
    /**Output
     * Name:zhangsan, Age:10
     * Name:lisi, Age:12
     *//
}

  1. static方法称为静态方法。静态方法不依赖与任何对象就可以惊醒访问,通过类名即可调用静态方法
public class Utils {
    public static void print(String s) {
        System.out.println("hello world: " + s);
    }

    public static void main(String[] args) {
        Utils.print("程序员Mylucy");
    }
}

  1. 静态代码块只会在类加载时执行一次。以下例子startDate和endDate在类加载时进行赋值
class Person  {
    private Date birthDate;
    private static Date startDate, endDate;
    static{
        startDate = Date.valueOf("2008");
        endDate = Date.valueOf("2021");
    }

    public Person(Date birthDate) {
        this.birthDate = birthDate;
    }
}

  1. 静态内部类。在静态方法中,使用非静态内部类依赖于外部类的实例,也就是说需要先创建外部类实例,才能用这个实例去创建非静态内部类。而静态内部类不需要。
public class OuterClass {
    class InnerClass {
    }
    static class StaticInnerClass {
    }
    public static void main(String[] args) {
        // 在静态方法里,不能直接使用OuterClass.this去创建InnerClass的实例
        // 需要先创建OuterClass的实例o,然后通过o创建InnerClass的实例
        // InnerClass innerClass = new InnerClass();
        OuterClass outerClass = new OuterClass();
        InnerClass innerClass = outerClass.new InnerClass();
        StaticInnerClass staticInnerClass = new StaticInnerClass();

        outerClass.test();
    }
    
    public void nonStaticMethod() {
        InnerClass innerClass = new InnerClass();
        System.out.println("nonStaticMethod...");
    }
}

  1. final
    1. 基本数据类型用fin修饰,则不能修改,是常量;对应引用用final修饰,则引用只能指向该对象,不能指向别的对象,但是对象本身能修改。
    2. final修饰的方法不能被子类重写
    3. final修饰的类不能被继承
  2. this
    1. this.属性名称 指访问类中的成员变量,可以用来区分成员变量和局部变量。如下代码所示,this.name 访问类Person当前实例的变量
public class Person {
    String name;
    int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

this.方法名称 用来访问本类的方法。在以下代码中,this.born()调用类Person的当前实例方法

public class Person {
    String name;
    int age;

    public Person(String name, int age) {
        this.born();
        this.name = name;
        this.age = age;
    }

    void born() {
    }
}

  2. supper:supper关键字用于在子类中访问父类的变量和方法
class A {
    protected String name = "Mylucy";

    public void getName() {
        System.out.println("父类:" + name);
    }
}

public class B extends A {
    @Override
    public void getName() {
        System.out.println(super.name);
        super.getName();
    }

    public static void main(String[] args) {
        B b = new B();
        b.getName();
    }
    /**
     * Mylucy
     * 父类:Mylucy
     */
}

在子类B中,我们重写了父类的getName()方法,如果在重写getName()方法中我们要调用父类相同的方法,必须通过supper关键字显式指出

final,finally,finalize的区别

  1. final 用于修饰属性、方法和类,分别表示属性不能被重写赋值,方法不可被覆盖,类不可被继承
  2. finally 是异常处理语句结构的一部分,一般以 try-catch-finally出现,finally代码块表示总被执行
  3. finalize 是Object类的一个方法,该方法一般由垃圾回收器调用,当我们调用Sytem.gc() 方法时,由垃圾回收器调用finalize() 方法,回收垃圾,JVM并不保证此方法总被调用

java中finally一定会被执行么

  1. 不一定
  2. 在以下两种情况finally不会被执行
    1. 程序未执行到tyr代码块
    2. 如果一个线程在执行try语句或者catch语句时被打断(interrupted)或者被终止(killed),与其相对应的finally语句块可能不会被执行。还有更极端的情况是:在线程运行try代码块或者catch语句块时,突然死机或者断点,finally语句块肯定不会执行

final关键字的作用

  1. final 修饰的类不能被继承
  2. final 修饰的方法不能被重写
  3. final 修饰的变量叫常量,常量必须初始化,初始化后的值不能被修改

方法重载和重写的区别

  1. 重载同个类中的多个方法可以有相同的方法名称,但是有不同的参数列表。参数列表又叫参数签名,包括参数的类型,参数个数,参数顺序,只要有一个不同就叫做参数列表不同
public class OverrideTest {
    void setPerson() { }
    
    void setPerson(String name) {
        //set name
    }
    
    void setPerson(String name, int age) {
        //set name and age
    }
}

  1. 重写:方法的重写描述的是父类和子类之间的。当父类的功能无法满足子类的需求,可以在子类的方法进行重写。当方法重写时,方法名和参数必须保持一致。如下代码,Person类作为父类,Student作为子类,在Student中重写了dailyTask方法
public class Person {
    private String name;
    
    public void dailyTask() {
        System.out.println("work eat sleep");
    }
}


public class Student extends Person {
    @Override
    public void dailyTask() {
        System.out.println("study eat sleep");
    }
}

接口与抽象类的区别

  1. 在语法层面上
    1. 抽象类可以有方法实现,而接口中只能是抽象方法(java8之后接口可以有默认实现)
    2. 抽象类中的成员变量可以是各种类型的,接口中的成员变量只能是public static final 类型
    3. 接口中不能含有静态代码块和静态方法,而抽象类可以有静态代码块和静态方法(java8之后可以有静态方法)
    4. 一个类只能继承一个抽象类,而一个类可以实现多个接口
  2. 设计层面上
    1. 抽象层次不同。抽象类是对整个类整体进行抽象,包括属性、行为,但是接口只是对类行为进行抽象。继承抽象类是一种“是不是”的关系,而接口实现则是“有没有”的关系。如果某一个类继承了某个抽象类,则子类必定是抽象类的种类,而接口实现则是具备不具备的关系。比如鸟是否能飞。
    2. 继承抽象类是具有相似特点的类,而实现接口的却可以不同的类
  3. 门和警报的例子
class AlarmDoor extends Door implements Alarm {
    //code
}

class BMWCar extends Car implements Alarm {
    //code
}

常见的Excption的有哪些

  1. 常见的RuntimExcption
    1. ClassCastExcption:类型转换异常
    2. IndexOutOfBoundsExcption:数组越界异常
    3. NullPointerExcption:空指针
    4. ArrayStroeExcption:数组存储异常
    5. NumberFormatExcption: 数字格式化异常
    6. ArithmeticExcption:数学以运算异常
  2. checked Excption
    1. NoSuchFieldExcption: 反射异常,没有对应字段
    2. ClassNotFoundExcption:类没有找到
    3. IllegalAccexxExcption:安全权限异常,可能是反射时调用了private方法

Error和Excption的区别

  1. Error:JVM无法解决的严重问题,如栈溢出(StackOverflowError),内存溢出(OOM)等程序无法处理的错误
  2. Excption:其他因编程错误或者偶然的哇你不因素导致的一般性能问题。可以在代码中处理。如空指针异常,数组下标越界等

运行时异常和非运行时异常(checked)的区别

unchecked excption 包括RuntimExcption和error类,其他所有异常称为检查(checked)异常

  1. RuntimExcption由程序错误导致,应该修正程序避免这类异常发生
  2. checked Exception由具体的环境(读取的文件不存在或文件为空或sql异常)导致的异常。必须进行处理,不然编译不通过,可以catch或者throws。

throw和throws的区别

  1. throw:用于抛出一个具体的异常对象
  2. throws:用于方法签名中,用于声明该方法可能抛出的异常。子类方法抛出的异常范围更小,或者根本不抛异常

什么是NIO

  1. 故事案例:
    1. 假设某银行存在10位员工,该银行的业务流程分为以下4个步骤
      1. 顾客填写申请表(5分钟)
      2. 职员审核(1分钟)
      3. 职员叫保安取钱(3分钟)
      4. 职员打印票据,并将钱给到顾客(1分钟)
    2. 以下是不同的模式对工作效率的影响
      1. BIO模式
        1. 每次来一位顾客,马上由一位职员接待处理,并且这个职员需要负责以上四个完整流程。当超过10个顾客时,剩余顾客需要排队等待
        2. 一位职员处理一位顾客需要10分钟(5+1+3+1),一个小时能够处理6个顾客,10个职员只能处理60个顾客。可以看出职员的工作状态并不饱和,其实在第一步当中就处于等待状态。
        3. 以上描述的工作模式其实就是BIO,每次来一个请求(顾客),就分配到线程池中由一个线程处理(职员),如果超出了线程池最大上限(10个),就扔到队列等待。在这种情况下,银行的吞吐量是小的。
        4. 如何提高银行的吞吐量?思路就是:分而治之,将任务拆开,由专门的人负责专门的事,具体描述如1.2.2.1
      2. NIO模式
        1. 银行专门指派一位职员A,A的工作就是每当有顾客到银行,就递上表格让顾客填写。每当由顾客填写好表格,A就将其随机指派给剩余的9名职员完成后续步骤。在这种方式下,假设顾客非常多,职员A的工作处于饱和当中,他不断的将填写好表格的顾客带到柜台处理,柜台一个职员5分钟能处理完一个顾客,一个小时9名职员就能处理:9*(60/5) = 108位顾客。
        2. 这种工作方式其实就是NIO思路,下图是非常经典的NIO说明图,mainReactor线程负责监听server socket,接收新连接,并将建立的socket分派给subReactor。subReactor可以是一个线程,也可以是线程池,负责多路分离已连接的scoket,读写网络数据。这里的读写网络数据可以类比顾客填写表格这一耗时动作。对具体的业务处理功能,交给worker线程池完成。不同的线程干不同的事情,术业有专攻,最终每个线程都没有空着

20220423154450.png

     3. 流程是否还存在可提高的地方
        1. 可以看到在这个业务流程中的第三个步骤,职员叫保安去金库取钱(3分钟),这三分钟柜台职员是在等待中的,可以把这三分钟利用起来,这就是分而治之的思路。每当柜台员工完成第二步时,就通知职员B负责与保安沟通取钱。这时候柜台员工可以继续处理下一个顾客。每当职员B拿到钱之后,就职顾客钱已经到柜台了,让顾客重新排队处理,当柜台职员再次服务该顾客时,发现该顾客前三步已经完成,直接执行第四步即可。在当今的web服务中,经常需要通过RPC或者Http等方式调用第三方服务,这里对应的就是第三步,如果这步耗时较长,通过异步方式将极大降低资源使用率。
        2. NIO+异步的方式能让少量的线程做大量的事情,这适用于很多引用场景,比如代理服务,api服务,长连接服务等等。这些引用如果用同步的方式将大量耗费机器资源。不过虽然NIO+异步能提高系统吞吐量,但其并不能让下一个请求的等待时间下降,相反可能会增加等待的耗时
     4. 最后NIO总结就是:分而治之,将任务拆分开来,由专门的人负责专门的事情。

BIO/NIO/AIO的区别

  1. 同步阻塞IO:用户发起一个进程IO之后,必须等待IO操作真正完成之后才能继续运行
  2. 不同非阻塞IO:客户端与服务器通过Channel连接,采用多路复用轮询注册的Channel。提高吞吐量和可靠性。用户进程发起一个IO操作以后,可以做其他事情,但用户进程必须轮询IO是否操作完成,这样会造成不惜要的CPU资源浪费。
  3. 异步非阻塞IO:非阻塞异步通信模式,NIO的升级,采用异步通道实现异步通信,其read和write方法均是异步方法。用户进程发起一个IO,然后立即返回,等IO真正操作完毕后,应用程序得到IO操作完成的通知。类似Future模式。

守护线程是什么

  1. 守护线程是运行在后台的一种特殊进程
  2. 它独立于控制终端并且周期性的执行某种任务或者等待处理某些发生的事情
  3. 在java中,垃圾回收线程就是特殊的守护线程

java支持多继承么

  1. java中,类不支持多继承,接口支持多继承
    1. 接口的作用是拓展对象功能,当一个子接口继承了多个父接口时,说明子接口拓展了多个功能。当一个类实现该接口时,就是拓展了多个功能
  2. java不支持多继承的原因:
    1. 处于安全性考虑,如果子类继承的多个父类里面存在相同的方法或者属性,子类将不知道具体要继承哪个
    2. java提供了接口和内部类以达到实现多继承功能,弥补单继承的缺陷

如何实现克隆对象

  1. 实现Clonable接口,重写clone();方法。这种方式是浅拷贝,即如果类中属性存在自定义引用类型,只拷贝引用,不拷贝引用指向的对象。如果对象的属性的class也实现了 Cloneable接口,那么克隆对象时也会克隆属性,即深拷贝。
  2. 结合序列化,深拷贝
  3. 通过org.apache.commons中的工具类BeanUtils和PropertyUtils进行对象复制。

同步和异步的区别是什么

  1. 同步:发出一个调用时,在没有得到结果之前,该调用不就返回。
  2. 异步:在调用发出之后,被调用者返回结果之后会通知调用者,或者通过回调函数处理这个调用

阻塞和非阻塞的区别

阻塞和非阻塞关注的是线程的状态。

  1. 阻塞调用是指调用结果之前,当前线程会被挂起。调用线程只有在得到结果之后才会恢复执行
  2. 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程
  3. 举例:烧开水
    :::info
    同步就是烧开水,要自己来看开没开。
    异步就是水开了,然后水壶响了通知你水开了(回调通知)。
    阻塞是烧开水的过程中,你不能干其他事情,必须在旁边等着。
    非阻塞是烧开水的过程里可以干其他事情。
    :::

java8的新特性有哪些

  1. Lambda表达式,Lambda允许把函数作为一个方法的参数
  2. Stream API:新添加的Stream API 把真正的函数式编程风格引入到java中
  3. 默认方法:默认方法就是在在一个接口中已经实现了一个方法default
  4. Optional类:Optinal类已经成为了java8类库的一部分,用来解决空指针异常
  5. Date Time API:加强日期与时间的处理

序列化和反序列化

序列化:把对象转换为字节序列的过程称为对象的序列化
反序列化:把字节序列恢复成对象的过程称为对象的反序列化

什么时候需要用到序列化和反序列化

当我们只在本地JVM运行java示例时,这时是不需要什么序列化和反序列化的,但当我们需要内存中的对象持久化到磁盘、数据库时,需要与浏览器进行交互时需要RPC时,这就需要进行序列化和反序列化。
前两个需要用到序列化和反序列化的场景,是不是让我们有一个很大的疑问? 我们在与浏览器交互时,还有将内存中的对象持久化到数据库中时,好像都没有去进行序列化和反序列化,因为我们都没有实现 Serializable 接口,但一直正常运行.
结论:
只要我们对内存总的对象进行持久化或网络传输,这个时候都需要进行序列化和反序列化
理由:
服务器与浏览器交互时真的没有用到Serializable接口码?JSON格式时间上就是将一个对象转化为字符串,所以服务器与浏览器交互时的数据格式其实是字符串,可以看String类型的源码:

public final class String
    implements java.io.SerializableComparable<String>CharSequence {
    /\*\* The value is used for character storage. \*/
    private final char value\[\];

    /\*\* Cache the hash code for the string \*/
    private int hash; // Default to 0

    /\*\* use serialVersionUID from JDK 1.0.2 for interoperability \*/
    private static final long serialVersionUID = -6849794470754667710L;

    ......
}

String类型实现了Serializable接口,并且显示指定serializableUID的值,然后再来看对象持久化到数据库中时的情况,Mybatis数据库映射文件里面的insert代码

<insert id="insertUser" parameterType="org.tyshawn.bean.User">
    INSERT INTO t\_user(name,age) VALUES (#{name},#{age})
</insert>

实际上我们并不是将整个对象持久化到数据库中,而是将对象中的属性持久化到数据库中,而这些属性(Date/String)都实现了Serializable接口

实现序列化和反序列化为什么需要实现Serializable接口

  1. 在java中实现了Serializable接口后,JVM在类加载时就会发现这个接口,然后在初始化实例对象的时候就会在底层帮我们实现序列化和反序列化
  2. 如果背写对象类型不是String、数组、Enum,并且没有实现Serializable接口,那么在序列化的时候将会抛出NotSerializableExcption。源码如下
// remaining cases
if (obj instanceof String) {
    writeString((String) obj, unshared);
} else if (cl.isArray()) {
    writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
    writeEnum((Enum<?>) obj, desc, unshared);
} else if (obj instanceof Serializable) {
    writeOrdinaryObject(obj, desc, unshared);
} else {
    if (extendedDebugInfo) {
        throw new NotSerializableException(
            cl.getName() + "\n" + debugInfoStack.toString());
    } else {
        throw new NotSerializableException(cl.getName());
    }
}

实现Serializable接口之后,为什么还需要指定serialVersionUID的值

  1. 如果不显示指定serializableUID,JVM在序列化时会根据属性自动生成一个serializableUID,然后与属性一起序列化,在进行持久化或网络传输。在序列化时,JVM会根据属性自动生成一个新版serializableUID,然后将这个新版的serializableUID与序列化时生成的旧版serializableUID进行比较,如果相同则反序列化成功,否则报错。
  2. 如果显示指定了serializableUID,VM在序列化和反序列化时仍然都会生成一个serializableUID,但值为我们显示指定的值,这样在反序列化时新旧版本的serializableUID就一致了
  3. 如果我们的类写完之后不在修改,那么不指定serializableUID,不会有问题,但这在实际开发中是不可能的,我们的类不会迭代,一旦类被修改了,那么旧对象反序列化就会报错,所以在实际开发中,我们都会显示指定一个serializableUID。

static属性为什么不会被序列化

  1. 因为序列化是针对对象而言,而static属性优先于对象存在,随着类的加载而加载,所以不会被序列化。
  2. 看到这个结论,是不是有人问,serializableUID也被static修饰,为什么serializableUID会被序列化?
    1. 其实serializableUID属性并没有被序列化,JVM在序列化对象时会自动生成一个serializableUID,然后将我们显示指定的serializableUID属性赋值给自动生成的serializableUID.

transient关键字的作用?

  1. java语言的关键字,变量修饰符,如果用transient声明一个实例变量,当对象存储时,它的值不需要维持
  2. 也就是说被transient修饰的成员变量,在序列化的时候该值会被忽略,在被反序列化后,transient变量的值被设为初始值,如果int型是0,对象型是null,

什么是反射

  1. 动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制
  2. 在运行状态中,对于任意一个类,能够知道这个类的所有属性和方法。对于任意一个对象,能够调用它的任意一个方法和属性。

反射的应用场景有哪些

  1. JDBC连接数据库时使用Class.forName() 通过反射加载数据库的驱动程序
  2. Eclispe、IDEA等开发工具利用反射动态解析对象的类型和结构,动态提示对象的属性和方法
  3. Web服务器中利用反射调用了Sevlet的 service方法
  4. JDK动态代理底层通过反射实现

什么是泛型

  1. java泛型时JDK5中引入的一个新特性,允许在定义类和接口的时候使用类型参数。声明的类型参数在使用时用具体的类型来替换。
  2. 泛型最大的好处是可以提高代码的复用性。以List接口为例,我们可以将String 、Integer等类型放入List中,如果不用泛型,存放String类型要写一个List接口,存在Interger类型的也要写一个接口。泛型可以很好的解决这个问题。

如何停止一个正在运行的线程

  1. 使用线程的stop 方法
    1. 使用stop() 方法可以强制终止线程。不过stop方法是一个被废弃掉的方法,不推荐使用
    2. 使用stop() 方法,会一直向上传播ThreadDeath异常,从而使得目标线程解锁所有锁住的监视器,即释放掉所有对象锁。使得之前被锁住的对象得不到同步处理,因此可能会造成数据不一致的问题
  2. 使用interrupt方法中断线程
    1. 该方法只是告诉线程要终止,但最终何时终止取决于计算器。调用interrupt方法仅仅是在当前线程中打了一个停止标记,并不是真的停止线程。
    2. 接着调用Thread.currentThread.interrupted() 方法,可以用来判断当前线程是否被终止,通过这个判断我们可以做一点业务逻辑处理,通常情况下,如果isInterrupted返回true的话,会抛出一个中断异常,然后通过try-catch捕获。
  3. 设置标志位
    1. 设置标志位,当标志位为某个值时,使线程正常退出。设置标志位是用到了共享变量的方式,为了保证共享变量在内存中的可见性,可以使用volatile修饰它,这样的话变量的取值始终会从主存中获取最新值。
    2. 但是这种volatile标记共享变量的方式,在线程发生阻塞时,是无法完成响应的。比如调用Thread.sleep() 方法之后,线程处于不可运行法状态,即便是主线程修改了共享变量的值,该线程此时根本无法检查循环标志,所以也无法实现线程中断。
      1. 因此interrupt() 加上手动抛异常的方式是目前中断一个正在运行的线程,最为正确的方式

什么是跨域

  1. 简单来说,跨域是指从一个域名的网页去请求另一个域名的资源。由于存在同源策略的关系,一般是不允许直接访问的。但是很多场景常常会存在跨域访问的需求,比如在前后端分离的情况下,前后端的域名是不一致的,此时就会发生跨域问题。
  2. 什么是同源策略?
    1. 所谓的“同源”是指“协议+域名+端口”三者相同,即便两个不同的域名指向同一个IP地址,也非同源
    2. 同源策略限制存在以下几个行为
      1. Cookie、LocalStorage和IndexDB无法读取
      2. DOM和js对象无法获得
      3. AJAX请求不能发生
  3. 为什么要有同源策略
    1. 举个例子,加入你刚刚在网银输入账号密码,查看了自己的余额,然后再去访问其他带颜色的网站这个网站可以访问刚刚的网银站点,并且获取账号密码,那后果可想而知。因此从安全角度来说,同源策略有利于保护网站信息

跨域问题如何解决

  1. CORS,跨域资源共享
    1. CORS(Cross-origin resource sgaring),跨域资源共享,CORS其实是浏览器制定了一个规范,浏览器会自动进行CORS通信,它的实现主要在服务端,通过一些HTTP Header来限制可以访问的域。例如A需要访问B服务器的数据,如果B服务器上声明了允许A的域名访问,那么从A到B的跨域请求就可以完成。
  2. @CrossOrigin注解
    1. 如果项目使用的是SpringBoot搭建,可以在controller类上添加@CrossOrigin(origins =““) 注解,该注解可以实现对当前controller的跨域访问。也可以添加在方法上,或者直接添加到入口类上,对所有接口进行跨域处理。注意,只有SpringMVC4.2以上的版本才支持@CrossOrigin(origins =””)注解
  3. nginx反向代理接口跨域
    1. nginx反向代理原理如下:
      1. 首先同源策略是浏览器的安全策略,不是HTTP协议的一部分。服务端调用HTTP接口只是使用HTTP协议,不会执行JS脚本,不需要同源策略,也就不存在跨域问题
      2. nginx反向代理接口实现原理如下:
        1. 通过nginx配置一个代理服务器(域名与domian1相同,端口不同)做跳板机,反向代理访问domian2接口,并且可以顺便修改cookie中domian信息,方便当前域cookie写入,实现跨域。这样前端代理只要访问http://domian1.com:8080/*就可以了。
// proxy服务器
server {
    listen       81;
    server_name  www.domain1.com;
    location / {
        proxy_pass   http://www.domain2.com:8080;  #反向代理
        proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
        index  index.html index.htm;
        
        add_header Access-Control-Allow-Origin http://www.domain1.com;
    }
}

  1. 通过jsonp跨域
    1. 通常为了减轻web服务的负载,我们把js、css、img等静态资源分离到另一台独立域名的服务器上,在html页面中再通过相应的标签从不同域名下加载静态资源,这是浏览器允许的操作,基于此原理,我们可以通过动态创建script,在请求一个带参网址实现跨域通信是。

设计接口需要注意什么

  1. 接口参数校验。接口必须校验参数,比如入参是否允许为空,入参长度是否符合预期。
  2. 设计接口时,充分考虑接口的可扩展性。思考接口是否可以复用,怎样保持接口的可扩展性。
  3. 串行调用考虑改并行调用。比如设计一个商城首页接口,需要查商品信息、营销信息、用户信息等等。如果是串行一个一个查,那耗时就比较大了。这种场景是可以改为并行调用的,降低接口耗时。
  4. 接口是否需要防重处理。涉及到数据库修改的,要考虑防重处理,可以使用数据库防重表,以唯一流水号作为唯一索引。
  5. 日志打印全面,入参出参,接口耗时,记录好日志,方便甩锅。
  6. 修改旧接口时,注意兼容性设计。
  7. 异常处理得当。使用finally关闭流资源、使用log打印而不是e.printStackTrace()、不要吞异常等等。
  8. 是否需要考虑限流。限流为了保护系统,防止流量洪峰超过系统的承载能力

过滤器和拦截器的区别

  1. 实现原理不同
    1. 过滤器和拦截器底层实现原理不同。过滤器是基于函数回调的,拦截器是基于java反射机制(动态代理)实现的。
    2. 一般自定义的过滤器中都会实现一个doFilter()方法,这个方法会有一个FliterChain参数,而实际上这是一个回调接口。
  2. 使用范围不同
    1. 过滤器实现的是javax.servlet.Filter接口,这个接口是在Servlet规范中定义的,也就是说过滤器Filter的使用需要依赖于Tomca等容器,导致它只能在Web程序中使用。而拦截器是一个Spring组件,并由Spring管理,并不依赖于Tomcat等容器,是可以单独使用的。拦截器不仅能应用在web程序中,也可以在Application,Swing等程序中。
  3. 使用场景不同
    1. 因为拦截器更接近业务系统,所以拦截器主要用来实现项目中的业务判断,比如:日志记录,权限判断等业务。而过滤器通常是用来实现通用功能过滤,比如敏感词过滤,响应数据压缩等功能
  4. 触发时机不同
    1. 过滤器是在请求进入容器后,但在进入servlet之前进行预处理,请求结束是在servlet处理完之后。
    2. 拦截器Interceptor是在请求进入servlet后,在进入controller之前进行预处理的,Controller中渲染了对应视图之后,请求结束
  5. 拦截的请求范围不同
    1. 请求的执行顺序是:请求进入容器 -> 进入过滤器 -> 进入servlet -> 进入拦截器 -> 执行控制器 。可以看到过滤器和拦截器的执行时机是不同的,过滤器会先执行,然后才会到拦截器,最后才会进入到真正执行的方法

对接第三方接口需要考虑什么

  1. 确认接口对接的网络协议,是https/http或者自定义的私有协议等。
  2. 约定好数据传参、响应格式(如application/json),弱类型对接强类型语言时要特别注意
  3. 接口安全方面,要确定身份校验方式,使用token、证书校验等
  4. 确认是否需要接口调用失败后的重试机制,保证数据传输的最终一致性。
  5. 日志记录要全面。接口出入参数,以及解析之后的参数值,都要用日志记录下来,方便定位问题(甩锅)。

后端接口性能优化有哪些方法

  1. 优化索引
    1. 给where条件的关键字段,或者order by后面的排序字段添加索引
  2. 优化SQL语句
    1. 避免使用select * 、批量操作,避免深分页,提升group by的效率
  3. 避免大事务
    1. 使用 @Transactional注解,这种声明式事务的方式提供事务功能,容易造成大事务,引发其他问题。应该避免在事务中一次性处理太多数据,将一些与事务无关的逻辑放到事务外执行。
  4. 异步处理
    1. 剥离主逻辑和副逻辑,副逻辑可以异步执行,异步写库。比如用户购买的商品发货了,需要发送短信通知,断行通知是副流程,可以异步执行,以免影响主流程的执行。
  5. 降低锁粒度
    1. 在并发场景下,多个线程同时修改数据,造成数据不一致的情况,这种情况下一般会加锁解决。但如果锁添加的不好,导致锁粒度太粗,也会非常影响接口性能是。
  6. 添加缓存
    1. 如果表数据量非常大的话,直接从数据库查询数据,性能会非常差。可以使用redis或者其他缓存框架提升查询性能,从而提高接口性能。
  7. 分库分表
    1. 当系统发展到一定阶段,用户并发量大,会有大量的数据库请求,需要占用大量的数据库连接,同时会带来磁盘IO的性能瓶颈问题。或者数据库表数据非常大,SQL查询即使走了索引,也非常耗时,这时可以考虑分库分表解决。
      1. 分库用于解决数据库连接资源不足的问题和磁盘IO的性能瓶颈问题。
      2. 分表用于解决单表数据量太大的问题,SQL语句查询数据时,即使走了索引也非常耗时。
  8. 避免在循环中查询数据库
    1. 循环查询数据库,非常耗时,最好在一次查询中获取所有需要的数据

为什么在阿里巴巴java开发手册中要求强制使用包装类型定义属性

  1. 以布尔字段为例,当我们没有设置对象和的字段的值时,Boolean类型的变量会设置默认值为null,而boolean类型的变量会设置默认值为false。也就是说包装类默认值都是null,而基本数据类型的默认值是一个固定值,如boolean是false,byte、short、int、long是0,float是0.0f等。
  2. 举一个例子,比如有一个扣费系统,扣费时需要从外部的定价系统中读取一个费率的值,我们预期该接口的返回值中会包含一个浮点型的费率字段。当我们取到这个值得时候就使用公式:金额*费率=费用 进行计算,计算结果进行划扣。如果由于计费系统异常,他可能会返回个默认值,如果这个字段是Double类型的话,该默认值为null,如果该字段是double类型的话,该默认值为0.0。如果扣费系统对于该费率返回值没做特殊处理的话,拿到null值进行计算会直接报错,阻断程序。拿到0.0可能就直接进行计算,得出接口为0后进行扣费了。这种异常情况就无法被感知。那我可以对0.0做特殊判断,如果是0就阻断报错,这样是否可以呢?不对,这时候就会产生一个问题,如果允许费率是0的场景又怎么处理呢?使用基本数据类型只会让方案越来越复杂,坑越来越多。这种使用包装类型定义变量的方式,通过异常来阻断程序,进而可以被识别到这种线上问题。如果使用基本数据类型的话,系统可能不会报错,进而认为无异常。因此,建议在POJO和RPC的返回值中使用包装类型。

如何让接口性能显著提升

  1. 池化思想
    1. 如果每次都需要用到线程池,都去创建,就会增加一定的耗时,而线程池可以重复利用线程,避免不必要的耗时
  2. 拒绝阻塞等待
    1. 如果一直调用一个系统B接口,但是特处理业务逻辑,耗时需要10s甚至跟多,然后调用方一直等待,直到系统B的下游接口返回再继续下一步操作,明显不合理。可以参考IO多路复用模型,即我们不等待系统B接口返回,而是先去做其他操作,等待系统B的接口处理完成,通过事件回调通知,我们接口收到通知在进行对应的业务操作。
  3. 远程调用由串行改为并行
  4. 锁粒度避免过粗
    1. 在高并发场景下,为了防止商品超卖,我们经常需要添加锁来保护共享资源,但是锁粒度过粗,是很影响接口性能的。无论是synchronized还是redis分布式锁,只需要在共享临界资源加锁即可,不涉及共享资源的就不必要加锁
  5. 耗时操作考虑异步
  6. 使用缓存
    1. 把要查的数据,提前放好到缓存里面,需要时,直接查缓存,而避免去查数据库或者计算的过程
  7. 提前初始化缓存
    1. 取思想很容易理解,就是提前把要计算查询的数据,初始化到缓存。如果你在未来某个时间需要用到某个经过复杂计算的数据,才实时去计算的话,可能耗时比较大。这时候,我们可以采取预取思想,提前把将来可能需要的数据计算好,放到缓存中,等需要的时候,去缓存取就行。这将大幅度提高接口性能
  8. 压缩传输内容
    1. 压缩传输内容,传输报文变得更小,因此传输会更快
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值