当前位置:   article > 正文

Java面试八股文整理

java面试八股文

一、Java基础

1. JRE和JDK的区别

  • 定义

    JRE(Java Runtime Enviroment) 是Java的运行环境。面向Java程序的使用者,而不是开发者。如果你仅下载并安装了JRE,那么你的系统只能运行Java程序。JRE是运行Java程序所必须环境的集合,包含JVM标准实现及 Java核心类库。它包括Java虚拟机、Java平台核心类和支持文件。它不包含开发工具(编译器、调试器等)。

    JDK(Java Development Kit) 又称J2SDK(Java2 Software Development Kit),是Java开发工具包,它提供了Java的开发环境(提供了编译器javac等工具,用于将java文件编译为class文件)和运行环境(提 供了JVM和Runtime辅助包,用于解析class文件使其得到运行)。如果你下载并安装了JDK,那么你不仅可以开发Java程序,也同时拥有了运 行Java程序的平台。JDK是整个Java的核心,包括了Java运行环境(JRE),一堆Java工具tools.jar和Java标准类库 (rt.jar)。

  • 区别

    JRE主要包含:java类库的class文件(都在lib目录下打包成了jar)和虚拟机(jvm.dll);

    JDK主要包含:java类库的 class文件(都在lib目录下打包成了jar)并自带一个JRE。那么为什么JDK要自带一个JRE呢?而且jdk/jre/bin下的client 和server两个文件夹下都包含jvm.dll(说明JDK自带的JRE有两个虚拟机)。

2. Java中equals()和==的区别

  • equals() 是判断两个变量或者实例指向同一个内存空间的值是不是相同
  • "==" 是判断两个变量或者实例是不是指向同一个内存空间
  • java中的数据类型,可分为两类:
    1. 基本数据类型,也称原始数据类型。byte,short,char,int,long,float,double,boolean
      他们之间的比较,应用双等号 “==” ,比较的是他们的
    2. 复合数据类型(类)
      当他们用 “==” 进行比较的时候,比较的是他们在 内存中的存放地址,所以,除非是同一个new出来的对象,他们的比较后的结果为true,否则比较后结果为false。 JAVA当中所有的类都是继承于Object这个基类的,在Object中的基类中定义了一个equals() 的方法,这个方法的初始行为是比较对象的内存地址,但在一些类库当中这个方法被覆盖掉了,如String,Integer,Date在这些类当中equals() 有其自身的实现,而不再是比较类在堆内存中的存放地址了。
      对于复合数据类型之间进行equals() 比较,在没有覆写equals() 方法的情况下,他们之间的比较还是基于他们在内存中的存放位置的地址值的,因为Object的equals() 方法也是用双等号 “==” 进行比较的,所以比较后的结果跟双等号 “==” 的结果相同。

3. 两个对象的hashCode()相同,则equals()也一定为true么?

  • 首先,答案肯定是不一定。同时反过来equals()为true,hashCode()也不一定相同。
  • 类的hashCode()方法和equals()方法都可以重写,返回的值完全在于自己定义。
  • hashCode()返回该对象的哈希码值;equals()返回两个对象是否相等。
  • 关于hashCode()和equals()方法是有一些常规协定 :
    1. 两个对象用equals()比较返回true,那么两个对象的hashCode()方法必须返回相同的结果。
    2. 两个对象用equals()比较返回false,不要求hashCode()方法也一定返回不同的值,但是最好返回不同值,以提高哈希表性能。
    3. 重写equals()方法,必须重写hashCode()方法,以保证equals方法相等时两个对象hashcode返回相同的值。

4. java中的final关键字

  • final关键字可以用来修饰引用、方法和类。

  • 用来修饰一个引用

    1. 如果引用为基本数据类型,则该引用为常量,该值无法修改;
    2. 如果引用为引用数据类型,比如对象、数组,则该对象、数组本身可以修改,但指向该对象或数组的地址的引用不能修改。
    3. 如果引用时类的成员变量,则必须当场赋值,否则编译会报错。
  • 用来修饰一个方法

    1. 当使用final修饰方法时,这个方法将成为最终方法,无法被子类重写。但是,该方法仍然可以被继承。
  • 用来修饰类

    1. 当用final修改类时,该类成为最终类,无法被继承。简称为“断子绝孙类”。

5. java 中操作字符串都有哪些类?它们之间有什么区别?

主要是一下三种:String、StringBuffer、StringBuilder

String和StringBuilder和StringBuffer的区别

StringStringBufferStringBuilder
String的值是不可变的,这就导致每次对String的操作都会生成新的String对象,不仅效率低下,而且浪费大量优先的内存空间StringBuffer是可变类,和线程安全的字符串操作类,任何对它指向的字符串的操作都不会产生新的对象。每个StringBuffer对象都有一定的缓冲区容量,当字符串大小没有超过容量时,不会分配新的容量,当字符串大小超过容量时,会自动增加容量可变类,速度更快
不可变可变可变
线程安全,因为 StringBuffer 的所有公开方法都是 synchronized 修饰的线程不安全
多线程操作字符串单线程操作字符串

6. String str = “i” 与 String str = new String(“i”)一样吗?

  • 不一样,因为内存的分配方式不一样。
    1. String str = "i"的方式,Java虚拟机会将其分配到常量池中;而String str = new String(“i”)则会被分到堆内存中。
    2. String str=“i”; 因为String 是final类型的,所以“i”应该是在常量池;而new String(“i”);则是新建对象放到堆内存中。

7. Java如何将字符串反转

  1. 利用 StringBuffer 或 StringBuilder 的 reverse 成员方法:

      // StringBuffer
      public static String reverse1(String str) {
        return new StringBuilder(str).reverse().toString();
      }
    
    • 1
    • 2
    • 3
    • 4
  2. 利用 String 的 toCharArray 方法先将字符串转化为 char 类型数组,然后将各个字符进行重新拼接:

     // toCharArray
      public static String reverse2(String str) {
        char[] chars = str.toCharArray();
        String reverse = "";
        for (int i = chars.length - 1; i >= 0; i--) {
          reverse += chars[i];
        }
        return reverse;
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
  3. 利用 String 的 CharAt 方法取出字符串中的各个字符:

      // charAt
      public static String reverse3(String str) {
        String reverse = "";
        int length = str.length();
        for (int i = 0; i < length; i++) {
          reverse = str.charAt(i) + reverse;
        }
        return reverse;
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

8. Java-String类的常用方法总结

  1. String类
    String类在java.lang包中,java使用String类创建一个字符串变量,字符串变量属于对象。java把String类声明的final类,不能有类。String类对象创建后不能修改,由0或多个字符组成,包含在一对双引号之间。

  2. String类对象的创建
    字符串声明:String stringName;
    字符串创建:stringName = new String(字符串常量);或stringName = 字符串常量;

  3. String类构造方法
    1、public String()
    无参构造方法,用来创建空字符串的String对象。

    String str1 = new String(); 
    
    • 1

    2、public String(String value)
    用已知的字符串value创建一个String对象。

    String str2 = new String("asdf"); 2 String str3 = new String(str2); 
    
    • 1

    3、public String(char[] value)
    用字符数组value创建一个String对象。

    char[] value = {'a','b','c','d'};
    String str4 = new String(value);//相当于String str4 = new String("abcd");
    
    • 1
    • 2

    4、public String(char chars[], int startIndex, int numChars)
    用字符数组chars的startIndex开始的numChars个字符创建一个String对象。

    char[] value = {'a','b','c','d'};
    String str5 = new String(value, 1, 2);//相当于String str5 = new String("bc");
    
    • 1
    • 2

    5、public String(byte[] values)
    用比特数组values创建一个String对象。

    byte[] strb = new byte[]{65,66};
    String str6 = new String(strb);//相当于String str6 = new String("AB");
    
    • 1
    • 2
  4. String类常用方法
    1、求字符串长度
    public int length()//返回该字符串的长度

    String str = new String("asdfzxc");
    int strlength = str.length();//strlength = 7
    
    • 1
    • 2

    2、求字符串某一位置字符
    public char charAt(int index)//返回字符串中指定位置的字符;注意字符串中第一个字符索引是0,最后一个是length()-1。

    String str = new String("asdfzxc");
    char ch = str.charAt(4);//ch = z
    
    • 1
    • 2

    3、提取子串
    用String类的substring方法可以提取字符串中的子串,该方法有两种常用参数:

    • public String substring(int beginIndex)//该方法从beginIndex位置起,从当前字符串中取出剩余的字符作为一个新的字符串返回。
    • public String substring(int beginIndex, int endIndex)//该方法从beginIndex位置起,从当前字符串中取出到endIndex-1位置的字符作为一个新的字符串返回。
    String str1 = new String("asdfzxc");
    String str2 = str1.substring(2);//str2 = "dfzxc"
    String str3 = str1.substring(2,5);//str3 = "dfz"
    
    • 1
    • 2
    • 3

    4、字符串比较

    • public int compareTo(String anotherString)//该方法是对字符串内容按字典顺序进行大小比较,通过返回的整数值指明当前字符串与参数字符串的大小关系。若当前对象比参数大则返回正整数,反之返回负整数,相等返回0。
    • public int compareToIgnore(String anotherString)//与compareTo方法相似,但忽略大小写。
    • public boolean equals(Object anotherObject)//比较当前字符串和参数字符串,在两个字符串相等的时候返回true,否则返回false。
    • public boolean equalsIgnoreCase(String anotherString)//与equals方法相似,但忽略大小写。
    String str1 = new String("abc");
    String str2 = new String("ABC");
    int a = str1.compareTo(str2);//a>0
    int b = str1.compareToIgnoreCase(str2);//b=0
    boolean c = str1.equals(str2);//c=false
    boolean d = str1.equalsIgnoreCase(str2);//d=true
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    5、字符串连接
    public String concat(String str)//将参数中的字符串str连接到当前字符串的后面,效果等价于"+"。

    String str = "aa".concat("bb").concat("cc");
    相当于String str = "aa"+"bb"+"cc";
    
    • 1
    • 2

    6、字符串中单个字符查找

    • public int indexOf(int ch/String str)//用于查找当前字符串中字符或子串,返回字符或子串在当前字符串中从左边起首次出现的位置,若没有出现则返回-1。
    • public int indexOf(int ch/String str, int fromIndex)//改方法与第一种类似,区别在于该方法从fromIndex位置向后查找。
    • public int lastIndexOf(int ch/String str)//该方法与第一种类似,区别在于该方法从字符串的末尾位置向前查找。
    • public int lastIndexOf(int ch/String str, int fromIndex)//该方法与第二种方法类似,区别于该方法从fromIndex位置向前查找。
    String str = "I am a good student";
    int a = str.indexOf('a');//a = 2
    int b = str.indexOf("good");//b = 7
    int c = str.indexOf("w",2);//c = -1
    int d = str.lastIndexOf("a");//d = 5
    int e = str.lastIndexOf("a",3);//e = 2
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    7、字符串中字符的大小写转换

    • public String toLowerCase()//返回将当前字符串中所有字符转换成小写后的新串
    • public String toUpperCase()//返回将当前字符串中所有字符转换成大写后的新串
    String str = new String("asDF");
    String str1 = str.toLowerCase();//str1 = "asdf"
    String str2 = str.toUpperCase();//str2 = "ASDF"
    
    • 1
    • 2
    • 3

    8、字符串中字符的替换

    • public String replace(char oldChar, char newChar)//用字符newChar替换当前字符串中所有的oldChar字符,并返回一个新的字符串。
    • public String replaceFirst(String regex, String replacement)//该方法用字符replacement的内容替换当前字符串中遇到的第一个和字符串regex相匹配的子串,应将新的字符串返回。
    • public String replaceAll(String regex, String replacement)//该方法用字符replacement的内容替换当前字符串中遇到的所有和字符串regex相匹配的子串,应将新的字符串返回。
    String str = "asdzxcasd";
    String str1 = str.replace('a','g');//str1 = "gsdzxcgsd"
    String str2 = str.replace("asd","fgh");//str2 = "fghzxcfgh"
    String str3 = str.replaceFirst("asd","fgh");//str3 = "fghzxcasd"
    String str4 = str.replaceAll("asd","fgh");//str4 = "fghzxcfgh"
    
    • 1
    • 2
    • 3
    • 4
    • 5

    9、其他类方法

    • String trim()//截去字符串两端的空格,但对于中间的空格不处理。
    String str = " a sd ";
    String str1 = str.trim();
    int a = str.length();//a = 6
    int b = str1.length();//b = 4
    
    • 1
    • 2
    • 3
    • 4
    • boolean statWith(String prefix)boolean endWith(String suffix)//用来比较当前字符串的起始字符或子字符串prefix和终止字符或子字符串suffix是否和当前字符串相同,重载方法中同时还可以指定比较的开始位置offset。
    String str = "asdfgh";
    boolean a = str.statWith("as");//a = true
    boolean b = str.endWith("gh");//b = true
    
    • 1
    • 2
    • 3
    • regionMatches(boolean b, int firstStart, String other, int otherStart, int length)//从当前字符串的firstStart位置开始比较,取长度为length的一个子字符串,other字符串从otherStart位置开始,指定另外一个长度为length的字符串,两字符串比较,当b为true时字符串不区分大小写。
    • contains(String str)//判断参数s是否被包含在字符串中,并返回一个布尔类型的值。
    String str = "student";
    str.contains("stu");//true
    str.contains("ok");//false
    
    • 1
    • 2
    • 3
    • String[] split(String str)//将str作为分隔符进行字符串分解,分解后的字字符串在字符串数组中返回。
    String str = "asd!qwe|zxc#";
    String[] str1 = str.split("!|#");//str1[0] = "asd";str1[1] = "qwe";str1[2] = "zxc";
    
    • 1
    • 2
  5. 字符串与基本类型的转换

    1. 字符串转换为基本类型
      java.lang包中有Byte、Short、Integer、Float、Double类的调用方法:

      public static byte parseByte(String s)
      public static short parseShort(String s)
      public static short parseInt(String s)
      public static long parseLong(String s)
      public static float parseFloat(String s)
      public static double parseDouble(String s)
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6

      例如:

      int n = Integer.parseInt("12");
      float f = Float.parseFloat("12.34");
      double d = Double.parseDouble("1.124");
      
      • 1
      • 2
      • 3
    2. 基本类型转换为字符串类型
      String类中提供了String valueOf()放法,用作基本类型转换为字符串类型。

      static String valueOf(char data[])
      static String valueOf(char data[], int offset, int count)
      static String valueOf(boolean b)
      static String valueOf(char c)
      static String valueOf(int i)
      static String valueOf(long l)
      static String valueOf(float f)
      static String valueOf(double d)
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8

      例如:

      String s1 = String.valueOf(12);
      String s1 = String.valueOf(12.34);
      
      • 1
      • 2
    3. 进制转换
      使用Long类中的方法得到整数之间的各种进制转换的方法:

      Long.toBinaryString(long l)
      Long.toOctalString(long l)
      Long.toHexString(long l)
      Long.toString(long l, int p)//p作为任意进制
      
      • 1
      • 2
      • 3
      • 4

9. 抽象类必须要有抽象方法吗?

  • 答:不需要,

    抽象类不一定有抽象方法;但是包含一个抽象方法的类一定是抽象类。(有抽象方法就是抽象类,是抽象类可以没有抽象方法)

  • 解释:

    • 抽象方法:

      java中的抽象方法就是以abstract修饰的方法,这种方法只声明返回的数据类型、方法名称和所需的参数,没有方法体,也就是说抽象方法只需要声明而不需要实现。

    • 抽象方法与抽象类:

      当一个方法为抽象方法时,意味着这个方法必须被子类的方法所重写,否则其子类的该方法仍然是abstract的,而这个子类也必须是抽象的,即声明为abstract。abstract抽象类不能用new实例化对象,abstract方法只允许声明不能实现。如果一个类中含有abstract方法,那么这个类必须用abstract来修饰,当然abstract类也可以没有abstract方法。 一个抽象类里面没有一个抽象方法可用来禁止产生这种类的对象。

    • Java中的抽象类:

      abstract class 在 Java 语言中表示的是一种继承关系,一个类只能使用一次继承关系。但是,一个类却可以实现多个interface。

      在abstract class 中可以有自己的数据成员,也可以有非abstarct的成员方法,而在interface中,只能够有静态的不能被修改的数据成员(也就是必须是static final的,不过在 interface中一般不定义数据成员),所有的成员方法都是abstract的。

10. 普通类和抽象类有哪些区别

  • 关键点:abstract修饰符(抽象方法)、具体实现过程、实例化、子类实现父类的抽象方法

    1. 普通类中不可含有抽象方法,可以被实例化;
    2. 抽象类,则抽象类中所有的方法自动被认为是抽象方法,没有实现过程,不可被实例化;
    3. 抽象类的子类,除非也是抽象类,否则必须实现该抽象类声明的方法

11. 抽象类可以使用final修饰吗?

  • 当然不可以,通过理解抽象类的作用我们就发现了,抽象类必须要被继承,如果用final修饰抽象类,这个抽象类就无法被继承,自然就无法使用了。

12. java中abstract类和interface的区别

  • 相同点
    1. 两者都是抽象类,都不能实例化。
    2. interface实现类及abstrct class的子类都必须要实现已经声明的抽象方法。
  • 不同点
    1. interface需要实现,要用implements,而abstract class需要继承,要用extends。
    2. 一个类可以实现多个interface,但一个类只能继承一个abstract class。
    3. interface强调特定功能的实现,而abstractclass强调所属关系。
    4. 尽管interface实现类及abstrct class的子类都必须要实现相应的抽象方法,但实现的形式不同。interface中的每一个方法都是抽象方法,都只是声明的(declaration,没有方法体),实现类必须要实现。而abstract class的子类可以有选择地实现。

13. Java中的IO流

指的是将不同的输入输出源通过流的形式进行输入或输出的操作,流是一种抽象的描述,在程序中指的是数据的一种转移方式。

  • IO流的分类:
    1. 按照数据的流向:
      输入流、输出流
    2. 按照流数据的格式:
      字符流、字节流
    3. 按照流数据的包装过程:
      节点流(低级流)、处理流(高级流)
  • 最基本的几种进行简单介绍:
    1. InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
    2. OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
  • Java文本文件读取的大致过程如下:
    1. 构建文件对象,
    2. 使用文件对象构造Reader对象可以是FileReaderInputStreamReaderRandomAccessFile
    3. 使用Reader对像构建BufferedReader对象(主要使用其**readLine()**方法,用于按行读取文件)
    4. 按行读取文件,将每行获取到的字符串进行处理。

14. BIO,NIO,AIO 有什么区别?

  • BIO (Blocking I/O): 同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。
  • NIO (New I/O): NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。NIO中的N可以理解为Non-blocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。
  • AIO (Asynchronous I/O): AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。AIO 是异步IO的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO操作本身是同步的。查阅网上相关资料,我发现就目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。

15. Files的常用方法有哪些?

  • Files.exists() 检测文件路径是否存在
  • Files.createFile()创建文件
  • Files.createDirectory()创建文件夹
  • Files.delete() 删除文件或者目录
  • Files.copy() 复制文件
  • Files.move() 移动文件
  • Files.size()查看文件个数
  • Files.read() 读取文件
  • Files.write()写入文件

16. 类加载过程

  1. 加载

    加载指的是将类的class文件读入到内存,并为之创建一个java.lang.Class对象,也就是说,当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象。

    类的加载由类加载器完成,类加载器通常由JVM提供,这些类加载器也是前面所有程序运行的基础,JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。

    通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下几种来源。

    • 从本地文件系统加载class文件,这是前面绝大部分示例程序的类加载方式。
    • 从JAR包加载class文件,这种方式也是很常见的,前面介绍JDBC编程时用到的数据库驱动类就放在JAR文件中,JVM可以从JAR文件中直接加载该class文件。
    • 通过网络加载class文件。
    • 把一个Java源文件动态编译,并执行加载。

    类加载器通常无须等到“首次使用”该类时才加载该类,Java虚拟机规范允许系统预先加载某些类。

  2. 链接

    当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中。类连接又可分为如下3个阶段。

    1. 验证:验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致。Java是相对C++语言是安全的语言,例如它有C++不具有的数组越界的检查。这本身就是对自身安全的一种保护。验证阶段是Java非常重要的一个阶段,它会直接的保证应用是否会被恶意入侵的一道重要的防线,越是严谨的验证机制越安全。验证的目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。其主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。

      四种验证做进一步说明:

      • ​ 文件格式验证:主要验证字节流是否符合Class文件格式规范,并且能被当前的虚拟机加载处理。例如:主,次版本号是否在当前虚拟机处理的范围之内。常量池中是否有不被支持的常量类型。指向常量的中的索引值是否存在不存在的常量或不符合类型的常量。
      • 元数据验证:对字节码描述的信息进行语义的分析,分析是否符合java的语言语法的规范。
      • 字节码验证:最重要的验证环节,分析数据流和控制,确定语义是合法的,符合逻辑的。主要的针对元数据验证后对方法体的验证。保证类方法在运行时不会有危害出现。
      • 符号引用验证:主要是针对符号引用转换为直接引用的时候,是会延伸到第三解析阶段,主要去确定访问类型等涉及到引用的情况,主要是要保证引用一定会被访问到,不会出现类等无法访问的问题。
    1. 准备:类准备阶段负责为类的静态变量分配内存,并设置默认初始值。

    2. 解析:将类的二进制数据中的符号引用替换成直接引用。说明一下:符号引用:符号引用是以一组符号来描述所引用的目标,符号可以是任何的字面形式的字面量,只要不会出现冲突能够定位到就行。布局和内存无关。直接引用:是指向目标的指针,偏移量或者能够直接定位的句柄。该引用是和内存中的布局有关的,并且一定加载进来的。

  3. 初始化

    初始化是为类的静态变量赋予正确的初始值,准备阶段和初始化阶段看似有点矛盾,其实是不矛盾的,如果类中有语句:private static int a = 10,它的执行过程是这样的,首先字节码文件被加载到内存后,先进行链接的验证这一步骤,验证通过后准备阶段,给a分配内存,因为变量a是static的,所以此时a等于int类型的默认初始值0,即a=0,然后到解析(后面在说),到初始化这一步骤时,才把a的真正的值10赋给a,此时a=10。

17. java中覆盖和重载的区别

  • 子类继承了父类,但重写了父类的方法,因此虽然是从父类中拿到的方法但重写之后与父类方法有了区别,因此称为覆盖(即子类方法覆盖了父类方法)
  • 重载的含义,一个类中可以有多个同名不同参(参数列表不同)的方法。
区别覆盖(override)重载(overload)
实现上子类对父类方法的重写同一个类中建立多个同名方法
参数与父类同名同参与别的方法同名不同参
返回子类与父类返回类型要一致无此要求
权限子类不能覆盖父类的private方法无此要求
父类一个方法只能在子类覆盖一次重载只要参数不同,可以多次
覆盖是针对父类方法的重写同类中的方法均可重载
重写要求子类比父类抛出更少的异常无此要求

18. Java 浅拷贝和深拷贝

19. Java的垃圾回收机制

垃圾收集GC(Garbage Collection)是Java语言的核心技术之一, 在Java中,程序员不需要去关心内存动态分配和垃圾回收的问题,这一切都交给了JVM来处理。

  1. 什么样的对象才是垃圾?

    这个问题其实很简单,对于Java对象来讲,如果说这个对象没有被其他对象所引用该对象就是无用的,此对象就被称为垃圾,其占用的内存也就要被销毁。那么自然而然的就引出了我们的第二个问题,判断对象为垃圾的算法都有哪些?

  2. 标记垃圾的算法

    Java中标记垃圾的算法主要有两种, 引用计数法可达性分析算法

    • 引用计数法

      引用计数法就是给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的,可以当做垃圾收集。这种方法实现起来很简单而且优缺点都很明显。

      • 优点 执行效率高,程序执行受影响较小
      • 缺点 无法检测出循环引用的情况,导致内存泄露
    • 可达性分析算法

      这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。

      那么什么对象可以作为GCRoot?

      • 虚拟机栈中的引用对象
      • 方法区中的常量引用对象
      • 方法区中的类静态属性引用对象
      • 本地方法栈中的引用对象
      • 活跃线程中的引用对象
  3. 如何将垃圾回收?

    在Java中存在着四种垃圾回收算法,标记清除算法复制算法标记整理算法以及分代回收算法。我们接下来会分别介绍他们。

    • 标记清除算法

      该算法分为“标记”和“清除”两个阶段:标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。它是最基础的收集算法,效率也很高,但是会带来两个明显的问题:

      1. 效率问题
      2. 空间问题(标记清除后会产生大量不连续的碎片)*
    • 复制算法

      为了解决效率问题,我们开发出了复制算法。它可以将内存分为大小相同的两块,每次 使用其中的一块。当第一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。

      简单来说就是该对象分为对象面以及空闲面,对象在对象面上创建,对象面上存活的对象会被复制到空闲面,接下来就可以清除对象面的内存。

      这种算法的优缺点也比较明显

      1. 优点:解决碎片化问题,顺序分配内存简单高效
      2. 缺点:只适用于存活率低的场景,如果极端情况下如果对象面上的对象全部存活,就要浪费一半的存储空间。
    • 标记整理算法

      为了解决复制算法的缺陷,充分利用内存空间,提出了标记整理算法。该算法标记阶段和标记清除一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。

    • 分代收集算法

      当前虚拟机的垃圾收集都采用分代收集算法,这种算法就是根据具体的情况选择具体的垃圾回收算法。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。

      比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。

20. Java文本文件读取的大致过程如下:

  1. 构建文件对象,
  2. 使用文件对象构造Reader对象可以是FileReaderInputStreamReaderRandomAccessFile
  3. 使用Reader对像构建BufferedReader对象(主要使用其**readLine()**方法,用于按行读取文件)
  4. 按行读取文件,将每行获取到的字符串进行处理。

21. 多线程

  • 有三种使用线程的方法:
    1. 实现 Runnable 接口;
    2. 实现 Callable 接口;
    3. 继承 Thread 类。

    实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。可以理解为任务是通过线程驱动从而执行的。

  • start()和run()的区别:
    1. start方法:

      通过该方法启动线程的同时也创建了一个线程,真正实现了多线程。无需等待run()方法中的代码执行完毕,就可以接着执行下面的代码。此时start()的这个线程处于就绪状态,当得到CPU的时间片后就会执行其中的run()方法。这个run()方法包含了要执行的这个线程的内容,run()方法运行结束,此线程也就终止了。

    2. run方法:

      通过run方法启动线程其实就是调用一个类中的方法,当作普通的方法的方式调用。并没有创建一个线程,程序中依旧只有一个主线程,必须等到run()方法里面的代码执行完毕,才会继续执行下面的代码,这样就没有达到写线程的目的。
      而run方法是业务逻辑实现的地方,本质上和任意一个类的任意一个成员方法并没有任何区别,可以重复执行,被一个线程反复调用,也可以被单独调用

  • 总结一下:
    1.start() 可以启动一个新线程,run()不能
    2.start()不能被重复调用,run()可以
    3.start()中的run代码可以不执行完就继续执行下面的代码,即进行了线程切换。直接调用run方法必须等待其代码全部执行完才能继续执行下面的代码。
    4.start() 实现了多线程,run()没有实现多线程。

22. Java反射机制

  • 一、什么是反射:

    1. Java反射机制的核心是在程序运行时动态加载类并获取类的详细信息,从而操作类或对象的属性和方法。本质是JVM得到class对象之后,再通过class对象进行反编译,从而获取对象的各种信息。
    2. Java属于先编译再运行的语言,程序中对象的类型在编译期就确定下来了,而当程序在运行时可能需要动态加载某些类,这些类因为之前用不到,所以没有被加载到JVM。通过反射,可以在运行时动态地创建对象并调用其属性,不需要提前在编译期知道运行的对象是谁。
  • 二、反射的原理:
    下图是类的正常加载过程、反射原理与class对象:

    Class对象的由来是将.class文件读入内存,并为之创建一个Class对象。

    img

  • 三、反射的优缺点:

    1. 优点:
      • 在运行时获得类的各种内容,进行反编译,对于Java这种先编译再运行的语言,能够让我们很方便的创建灵活的代码,这些代码可以在运行时装配,无需在组件之间进行源代码的链接,更加容易实现面向对象。
    2. 缺点:
      • 反射会消耗一定的系统资源,因此,如果不需要动态地创建一个对象,那么就不需要用反射;
      • 反射调用方法时可以忽略权限检查,因此可能会破坏封装性而导致安全问题。
  • 四、反射的用途:

    1. 反编译:.class–>.java
    2. 通过反射机制访问java对象的属性,方法,构造方法等
    3. 当我们在使用IDE,比如Ecplise时,当我们输入一个对象或者类,并想调用他的属性和方法是,一按点号,编译器就会自动列出他的属性或者方法,这里就是用到反射。
    4. 反射最重要的用途就是开发各种通用框架。比如很多框架(Spring)都是配置化的(比如通过XML文件配置Bean),为了保证框架的通用性,他们可能需要根据配置文件加载不同的类或者对象,调用不同的方法,这个时候就必须使用到反射了,运行时动态加载需要的加载的对象。

二、Java容器

1. Java中常见的容器有哪些?

常用容器可分为Collection和Map,Collection是存储对象的集合,而Map是存储键值对的集合。
其中,Collection又分为List、Set、Queue,而Map的实现类为HashMap、LinkedHashMap、TreeMap、HashTable。

  • List接口(有序,可重复):
    • ArrayList:底层是动态数组,支持随机访问。
    • LinkedList:底层是双向链表,只能顺序访问。
  • Set接口(不可重复):
    • HashSet(无序):基于哈希表。支持快速查找,但不支持有序性操作,且不维持插入顺序信息。
    • TreeSet(有序):底层是红黑树。支持快速查找(O(logn))但效率比HashSet(O(1))低。支持有序性操作,例如在一定范围内查找元素。
    • LinkedHashSet(有序):底层是链表+哈希表。使用哈希表存储元素,再维护一个双向链表保存元素的插入信息。
  • Queue接口:
    • LinkedList:可实现双向队列
    • PriorityQueue:基于堆结构的优先队列。
  • Map接口:
    • HashMap:基于哈希表。
    • LinkedHashMap:使用双向链表维护插入顺序。
    • HashTable:线程安全的HashMap,已淘汰。推荐ConcurrentHashMap。
    • TreeMap:基于红黑树。
  • 常用集合的分类:
  • Collection 接口的接口 对象的集合(单列集合)
    ├——-List 接口:元素按进入先后有序保存,可重复
    │—————-├ LinkedList 接口实现类, 链表, 插入删除, 没有同步, 线程不安全
    │—————-├ ArrayList 接口实现类, 数组, 随机访问, 没有同步, 线程不安全
    │—————-└ Vector 接口实现类 数组, 同步, 线程安全
    │ ———————-└ Stack 是Vector类的实现类
    └——-Set 接口: 仅接收一次,不可重复,并做内部排序
    ├—————-└HashSet 使用hash表(数组)存储元素
    │————————└ LinkedHashSet 链表维护元素的插入次序
    └ —————-TreeSet 底层实现为二叉树,元素排好序

  • Map 接口 键值对的集合 (双列集合)
    ├———Hashtable 接口实现类, 同步, 线程安全
    ├———HashMap 接口实现类 ,没有同步, 线程不安全-
    │—————–├ LinkedHashMap 双向链表和哈希表实现
    │—————–└ WeakHashMap
    ├ ——–TreeMap 红黑树对所有的key进行排序
    └———IdentifyHashMap

2. int与integer的区别

  • Integer是int提供的封装类,而int是java的基本数据类型
  • Integer默认值是null,而int默认值是0;
  • 声明为Integer的变量需要实例化,而声明为int的变量不需要实例化
  • Integer是对象,用一个引用指向这个对象,而int是基本类型,直接存储数据
  • 类似的还有:float Float;double Double;string String等
  • 举个例子:当需要往ArrayList,HashMap中放东西时,像int,double这种内建类型是放不进去的,因为容器都是装 object的,这是就需要这些内建类型的外覆类了。
  • Java中每种内建类型都有相应的外覆类

3. Array 和ArrayList的区别

  1. 长度的区别:

    • Array是数组,声明好之后,其长度就已经固定。

      ArrayList底层是用数组实现的,但是ArrayList的长度是可变的,在每次添加时,如果发现空间不足的话,会创建一个长度大概是原来1.5倍的新数组(java8源码),然后把原来的数组元素复制过去。

  2. 存放数据的区别:

    • Array可以除了可以存放对象类型的数据之外,还可以存放基本数据类型的数据。

      ArrayList只能存放对象数据类型的数据,因为它的类在定义时已经是针对Object的子类做了泛型的约束。

4. HashTable和HashMap区别

  • 继承的父类不同

    Hashtable继承自Dictionary类,而HashMap继承自AbstractMap类。但二者都实现了Map接口。

  • 线程安全性不同

    javadoc中关于hashmap的一段描述如下:此实现不是同步的。如果多个线程同时访问一个哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须保持外部同步。

    Hashtable 中的方法是Synchronized的,而HashMap中的方法在缺省情况下是非Synchronize的。在多线程并发的环境下,可以直接使用Hashtable,不需要自己为它的方法实现同步,但使用HashMap时就必须要自己增加同步处理。(结构上的修改是指添加或删除一个或多个映射关系的任何操作;仅改变与实例已经包含的键关联的值不是结构上的修改。)这一般通过对自然封装该映射的对象进行同步操作来完成。如果不存在这样的对象,则应该使用 Collections.synchronizedMap 方法来“包装”该映射。最好在创建时完成这一操作,以防止对映射进行意外的非同步访问,如下所示:

  • 是否提供contains方法

    HashMap把Hashtable的contains方法去掉了,改成containsValue和containsKey,因为contains方法容易让人引起误解。

    Hashtable则保留了contains,containsValue和containsKey三个方法,其中contains和containsValue功能相同。

  • key和value是否允许null值

    其中key和value都是对象,并且不能包含重复key,但可以包含重复的value。

    通过上面的ContainsKey方法和ContainsValue的源码我们可以很明显的看出:

    Hashtable中,key和value都不允许出现null值。但是如果在Hashtable中有类似put(null,null)的操作,编译同样可以通过,因为key和value都是Object类型,但运行时会抛出NullPointerException异常,这是JDK的规范规定的。
    HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null。当get()方法返回null值时,可能是 HashMap中没有该键,也可能使该键所对应的值为null。因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个键, 而应该用containsKey()方法来判断。

  • 两个遍历方式的内部实现上不同

    Hashtable、HashMap都使用了 Iterator。而由于历史原因,Hashtable还使用了Enumeration的方式 。

  • hash值不同

    哈希值的使用不同,HashTable直接使用对象的hashCode而HashMap重新计算hash值

    hashCode是jdk根据对象的地址或者字符串或者数字算出来的int类型的数值。

    Hashtable计算hash值,直接用key的hashCode(),而HashMap重新计算了key的hash值, Hashtable在求hash值对应的位置索引时,用取模运算,而HashMap在求位置索引时,则用与运算,且这里一般先用hash&0x7FFFFFFF后,再对length取模,&0x7FFFFFFF的目的是为了将负的hash值转化为正值,因为hash值有可能为负数,而&0x7FFFFFFF后,只有符号外改变,而后面的位都不变。

  • 内部实现使用的数组初始化和扩容方式不同

    HashTable在不指定容量的情况下的默认容量为11,而HashMap为16,Hashtable不要求底层数组的容量一定要为2的整数次幂,而HashMap则要求一定为2的整数次幂。
    Hashtable扩容时,将容量变为原来的2倍加1,而HashMap扩容时,将容量变为原来的2倍。

    Hashtable和HashMap它们两个内部实现方式的数组的初始大小和扩容的方式。HashTable中hash数组默认大小是11,增加的方式是 old*2+1。

5. 数组和链表的区别?

  • 不同:
    1. 链表是链式的存储结构;数组是顺序的存储结构。
    2. 链表通过指针来连接元素与元素,数组则是把所有元素按次序依次存储。
    3. 链表的插入删除元素相对数组较为简单,不需要移动元素,且较为容易实现长度扩充,但是寻找某个元素较为困难;数组寻找某个元素较为简单,但插入与删除比较复杂,由于最大长度需要再编程一开始时指定,故当达到最大长度时,扩充长度不如链表方便。
  • **相同:**两种结构均可实现数据的顺序存储,构造出来的模型呈线性结构。

6. hashmap的实现:数组+链表+红黑树

  • 数组+链表:数组寻址容易,插入删除难;链表寻址难,插入删除容易

  • 链表长度超过8,转变为使用红黑树

  • 存put(k,v):首先把(k,v)封装到Node对象;然后调用hashCode()方法计算k的hash值;最后通过哈希算法将hash值转换成数组下标,下标上如果没有元素就把Node添加到该位置,如果下标对应位置有链表,比较链表中每个key的equals()返回,若全为false则添加到链表末尾,若有一个返回true则覆盖

  • 红黑树:
    1. 节点要么黑,要么红
    2. 根节点为黑
    3. 空的(NIL/NULL)的叶子节点为黑
    4. 红节点的子节点必须为黑
    5. 从一个节点到该节点的子孙节点的所有路劲包含相同数目的黑节点
    6. 保证红黑树的方法:变色,旋转(左旋转and右旋转)

7. 线程池

​ 又以上介绍我们可以看出,在一个应用程序中,我们需要多次使用线程,也就意味着,我们需要多次创建并销毁线程。而创建并销毁线 程的过程势必会消耗内存。而在Java中,内存资源是及其宝贵的,所以,我们就提出了线程池的概念。

  • 线程池: Java中开辟出了一种管理线程的概念,这个概念叫做线程池,从概念以及应用场景中,我们可以看出,线程池的好处,就是可以方便的管理线程,也可以减少内存的消耗。

    那么,我们应该如何创建一个线程池那?Java中已经提供了创建线程池的一个类:Executor

    而我们创建时,一般使用它的子类:ThreadPoolExecutor.

    public ThreadPoolExecutor(int corePoolSize,  
                                  int maximumPoolSize,  
                                  long keepAliveTime,  
                                  TimeUnit unit,  
                                  BlockingQueue<Runnable> workQueue,  
                                  ThreadFactory threadFactory,  
                                  RejectedExecutionHandler handler)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这是其中最重要的一个构造方法,这个方法决定了创建出来的线程池的各种属性,线程池中的corePoolSize就是线程池中的核心线程数量,这几个核心线程,只是在没有用的时候,也不会被回收,maximumPoolSize就是线程池中可以容纳的最大线程的数量,而keepAliveTime,就是线程池中除了核心线程之外的其他的最长可以保留的时间,因为在线程池中,除了核心线程即使在无任务的情况下也不能被清除,其余的都是有存活时间的,意思就是非核心线程可以保留的最长的空闲时间,而util,就是计算这个时间的一个单位,workQueue,就是等待队列,任务可以储存在任务队列中等待被执行,执行的是FIFIO原则(先进先出)。threadFactory,就是创建线程的线程工厂,最后一个handler,是一种拒绝策略,我们可以在任务满了知乎,拒绝执行某些任务。

  • 线程池的执行流程又是怎样的呢?

    有图我们可以看出,任务进来时,首先执行判断,判断核心线程是否处于空闲状态,如果不是,核心线程就先就执行任务,如果核心线程已满,则判断任务队列是否有地方存放该任务,若果有,就将任务保存在任务队列中,等待执行,如果满了,在判断最大可容纳的线程数,如果没有超出这个数量,就开创非核心线程执行任务,如果超出了,就调用handler实现拒绝策略。

  • handler的拒绝策略:
    • 第一种AbortPolicy:不执行新任务,直接抛出异常,提示线程池已满
    • 第二种DisCardPolicy:不执行新任务,也不抛出异常
    • 第三种DisCardOldSetPolicy:将消息队列中的第一个任务替换为当前新进来的任务执行
    • 第四种CallerRunsPolicy:直接调用execute来执行当前任务
  • 四种常见的线程池:
    • CachedThreadPool:可缓存的线程池,该线程池中没有核心线程,非核心线程的数量为Integer.max_value,就是无限大,当有需要时创建线程来执行任务,没有需要时回收线程,适用于耗时少,任务量大的情况。
    • SecudleThreadPool:周期性执行任务的线程池,按照某种特定的计划执行线程中的任务,有核心线程,但也有非核心线程,非核心线程的大小也为无限大。适用于执行周期性的任务。
    • SingleThreadPool:只有一条线程来执行任务,适用于有顺序的任务的应用场景。
    • FixedThreadPool:定长的线程池,有核心线程,核心线程的即为最大的线程数量,没有非核心线程

8. HashMap和HashSet的区别

  • 什么是HashSet

    HashSet实现了Set接口,它不允许集合中有重复的值,当我们提到HashSet时,第一件事情就是在将对象存储在HashSet之前,要先确保对象重写equals()和hashCode()方法,这样才能比较对象的值是否相等,以确保set中没有储存相等的对象。如果我们没有重写这两个方法,将会使用这个方法的默认实现。

    public boolean add(Object o)
    //方法用来在Set中添加元素,当元素值重复时则会立即返回false,如果成功添加的话会返回true。
    
    • 1
    • 2
  • 什么是HashMap

    HashMap实现了Map接口,Map接口对键值对进行映射。Map中不允许重复的键。Map接口有两个基本的实现,HashMap和TreeMap。TreeMap保存了对象的排列次序,而HashMap则不能。HashMap允许键和值为null。HashMap是非synchronized的,但collection框架提供方法能保证HashMap synchronized,这样多个线程同时访问HashMap时,能保证只有一个线程更改Map。

    public Object put(Object Key,Object value)
    //方法用来将元素添加到map中。
    
    • 1
    • 2

    你可以阅读这篇文章看看HashMap的工作原理,以及这篇文章看看HashMap和HashTable的区别。

  • HashSet和HashMap的区别

HashMapHashSet
HashMap实现了Map接口HashSet实现了Set接口
HashMap储存键值对HashSet仅仅存储对象
使用put()方法将元素放入map中使用add()方法将元素放入set中
HashMap中使用键对象来计算hashcode值HashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性,如果两个对象不同的话,那么返回false
HashMap比较快,因为是使用唯一的键来获取对象HashSet较HashMap来说比较慢

三、数据结构和算法

1. 二叉树的特性 时间复杂度计算过程写一下

二叉树是一棵树,且每个节点都不能有多于两个的儿子,且二叉树的子树有左右之分,次序不能颠倒。

  • 二叉树的性质

    • 在二叉树中的第i层上至多有2^(i-1)个结点(i>=1)。
    • 深度为k的二叉树至多有2^k - 1个节点(k>=1)。
    • 对任何一棵二叉树T,如果其叶结点数目为n0,度为2的节点数目为n2,则n0=n2+1。
  • **满二叉树:**深度为k且具有2^k-1个结点的二叉树。即满二叉树中的每一层上的结点数都是最大的结点数。

  • **完全二叉树:**深度为k具有n个结点的二叉树,当且仅当每一个结点与深度为k的满二叉树中的编号从1至n的结点一一对应。

  • 具有n个节点的完全二叉树的深度为log2n + 1。

二叉搜索树,平衡二叉树,红黑树的算法效率

操作二叉查找树平衡二叉树红黑树
查找O(n)O(logn)Olog(n)
插入O(n)O(logn)Olog(n)
删除O(n)O(logn)Olog(n)

Olog(n)怎么算出来的

在一个树中查找一个数字,
第一次在根节点判断,第二次在第二层节点判断
以此类推,树的高度是多少就会判断多少次
树的高度和节点的关系就是以2为底,树的节点总数n的对数
  • 1
  • 2
  • 3
  • 4

2. 手写二分 有序正负数组找到近 0 的两个数

public static int[] divide(int[] array){ 
    int mid = (min + max) / 2;
    int[] result = new int[2];
    while(array[mid] != 0){
        if(array[mid] > 0){
            max = mid - 1;
        }
        if(array[mid] < 0){
            min = mid + 1;
        }
        if(min >= max){
            break;
        }
        mid = (min + max) / 2;
    }
    result[0] = array[mid - 1];
    result[1] = array[mid + 1];
    return result;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

3. 二叉树三种遍历:

public class TreeNode{
    int val;
    public TreeNode(va){
        val = va;
    }
    TreeNode left;
    TreeNode right;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

前序遍历:(根左右)

/*递归法*/
ArrayList<Integer> pre = new ArrayList<>();
public ArrayList<Integer> DLR(TreeNode root){
    if(root == null){
        return pre;
    }
    pre.add(root.val);
    DLR(root.left);
    DLR(root.right);
    
    return pre;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
/*非递归法
1、申请一个栈stack,然后将头节点压入stack中。

2、从stack中弹出栈顶节点,打印,再将其右孩子节点(不为空的话)先压入stack中,最后将其左孩子节点(不为空的话)压入stack中。

3、不断重复步骤2,直到stack为空,全部过程结束。*/
public List<Integer> preorderTraversal(TreeNode root) {
    List<Integer> list=new ArrayList<Integer>();
    Stack<TreeNode> stack=new Stack<TreeNode>();
    if (root!=null) {
        stack.push(root);
        while(!stack.empty()) {
            TreeNode tr=stack.pop();
            list.add(tr.val);
            if(tr.right!=null) {
                stack.push(tr.right);
            }
            if(tr.left!=null) {
                stack.push(tr.left);
            }
        }
    }
    return list;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

中序遍历:(左根右)

/*递归法*/
ArrayList<Integer> mid = new ArrayList<>();
public ArrayList<Integer> LDR(TreeNode root){
    if(root == null){
        return mid;
    }
    LDR(root.left);
    mid.add(root.val);
    LDR(root.right);
    
    return mid;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
/*非递归法
1、申请一个栈stack,初始时令cur=head

2、先把cur压入栈中,依次把左边界压入栈中,即不停的令cur=cur.left,重复步骤2

3、不断重复2,直到为null,从stack中弹出一个节点,记为node,打印node的值,并令cur=node.right,重复步骤2

4、当stack为空且cur为空时,整个过程停止。*/
public List<Integer> inorderTraversal(TreeNode head) {
    List<Integer> list=new ArrayList<Integer>();
    Stack<TreeNode> stack=new Stack<TreeNode>();
    if (head!=null) {
        while(head!=null||!stack.empty()) {
            if(head!=null) {
                stack.push(head);
                head=head.left;
            }else {
                head=stack.pop();
                list.add(head.val);
                head=head.right;
            }
        }
    }
    return list;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

后序遍历:(左右根)

/*递归法*/
ArrayList<Integer> post = new ArrayList<>();
public ArrayList<Integer> LRD(TreeNode root){
    if(root == null){
        return post;
    }
    LRD(root.left);
    LRD(root.right);
    post.add(root.val);
    
    return post
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
/*非递归法
用非递归的方式实现后序遍历有点麻烦。

1、申请一个栈s1,然后将头节点压入栈s1中。

2、从s1中弹出的节点记为cur,然后依次将cur的左孩子节点和右孩子节点压入s1中。

3、在整个过程中,每一个从s1中弹出的节点都放进s2中。

4、不断重复步骤2和步骤3,直到s1为空,过程停止。

5、从s2中依次弹出节点并打印,打印的顺序就是后序遍历的顺序。*/

public List<Integer> postorderTraversal(TreeNode head) {
    List<Integer> list = new ArrayList<Integer>();
    Stack<TreeNode> stack1 = new Stack<TreeNode>();
    Stack<TreeNode> stack2 = new Stack<TreeNode>();
    if (head != null) {
        stack1.push(head);
        while(!stack1.empty()) {
            head = stack1.pop();
            stack2.push(head);
            if (head.left != null) {
                stack1.push(head.left);
            }
            if (head.right != null) {
                stack1.push(head.right);
            }
        }
        while(!stack2.empty()) {
            list.add(stack2.pop().val);
        } 
    }
    return list;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

4. 队列模拟栈:

Class QueueToStack{
    Queue<Integer> queue1 = new LinkedList<>();
    Queue<Integer> queue2 = new LinkedList<>();
    public void push(int x){
        if (queue1.isEmpty() && queue2.isEmpty()){
            queue1.add(x);
        }
        else if(queue1.isEmpty() && !queue2.isEmpty()){
            queue1.add(x);
            while (!queue2.isEmpty()){
                queue1.add(queue2.remove());
            }
        }
        else if(!queue1.isEmpty() && queue2.isEmpty()){
            queue2.add(x);
            while (!queue1.isEmpty()){
                queue2.add(queue1.remove());
            }
        }
    }

    public int pop(){
        return queue1.isEmpty() ? queue2.remove() : queue1.remove();
    }
}


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

四、Linux基本命令

  • pwd 显示工作路径

  • chmod 777和754 修改文件权限

    读取权限 r = 4
    写入权限 w = 2
    执行权限 x = 1

  • 查看进程状态ps,查看cpu状态 top。查看占用端口的进程号netstat grep

  • ps aux | less 输入下面的ps命令,显示所有运行中的进程

  • pgrep:通过程序的名字来查询进程,默认只显示PID

  • pidof: 根据程序名称,查找其相关进程的ID号

  • lsof -i:端口号:查看端口占用情况

  • top :动态实时显示cpu、内存、进程等使用情况(类似windows下的任务管理器)

  • kill -9 进程号 :强制杀死进程

  • renice NI PID:调整已经启动的进程的nice值

  • nice -n NI COMMAND:在启动时指定nice值

  • tar -zxvf archive.tar.gz 解压一个gzip格式的压缩包

  • sed ‘s/stringa1/stringa2/g’ example.txt 将example.txt文件中的 “string1” 替换成 “string2”

  • sed ‘/^$/d’ example.txt 从example.txt文件中删除所有空白行

  • *sed '/ #/d; /^$/d’ example.txt 从example.txt文件中删除所有注释和空白行

  • echo ‘esempio’ | tr ‘[:lower:]’ ‘[:upper:]’ 合并上下单元格内容

  • sed -e ‘1d’ result.txt 从文件example.txt 中排除第一行

  • sed -n ‘/stringa1/p’ 查看只包含词汇 "string1"的行

  • *sed -e 's/ $//’ example.txt 删除每一行最后的空白字符

  • sed -e ‘s/stringa1//g’ example.txt 从文档中只删除词汇 “string1” 并保留剩余全部

  • sed -n ‘1,5p;5q’ example.txt 查看从第一行到第5行内容

  • sed -n ‘5p;5q’ example.txt 查看第5行

  • sed -e 's/00/0/g’ example.txt* 用单个零替换多个零

  • cat file1 | command( sed, grep, awk, grep, etc…) > result.txt 合并一个文件的详细说明文本,并将简介写入一个新文件中

  • cat file1 | command( sed, grep, awk, grep, etc…) >> result.txt 合并一个文件的详细说明文本,并将简介写入一个已有的文件中

  • grep Aug /var/log/messages 在文件 '/var/log/messages’中查找关键词"Aug"

  • grep ^Aug /var/log/messages 在文件 '/var/log/messages’中查找以"Aug"开始的词汇

五、数据库

1. MySQL数据库设计三范式

  • 1NF:字段不可分;
  • 2NF:有主键,非主键字段依赖主键;
  • 3NF:非主键字段不能相互依赖;

解释:

  • 1NF:原子性 字段不可再分,否则就不是关系数据库;

  • 2NF:唯一性 一个表只说明一个事物;

  • 3NF:每列都与主键有直接关系,不存在传递依赖;

    基本sql语句

    1.自然连接(natural join)
    自然连接将表中具有相同名称的列自动进行匹配,自然连接不必指定任何同等连接条件也不能认为指定哪些列需要被匹配,自然连接得到的结果表中,两表中名称相同的列只出现一次。

    select * from employee natural join department;
    
    • 1

    2.内连接(inner join):产生的结果是A和B的交集(相同列里面的相同值)
    内连接查询能将左表和右表中能关联起来的数据连接后返回,返回的结果就是两个表中所有相匹配的数据。

    select * from TableA as A inner join TableB B on A.PK = B.PK;
    select * from TableA as A inner join TableB B on A.PK > B.PK;
    
    • 1
    • 2

    img
    3.外连接(outer join)
    内连接是要显示两张表的内存,而外连接不要求如此,外连接可以依据连接表保留左表,右表或全部表的行为而分为左外连接右外连接和全连接。

    select * from TableA as A left(right/full) join TableB as B on A.PA = B.PK;
    
    • 1

    Full Join:产生的结果是A和B的并集(如果没有相同的值会用null作为值)

    img
    Left Join:产生表A的完全集,而B表中匹配的则有值(没有匹配的则以null值取代)
    img
    Right Join:产生表B的完全集,而A表中匹配的则有值(没有匹配的则以null值取代)

    img
    4.交叉连接(cross join)
    又称笛卡尔连接,交叉连接返回两个集合的笛卡尔积。

    select * from TableA cross join TableB;
    
    • 1
  • 左连接:

    SELECT
    song.`name`
    FROM
    song
    LEFT JOIN
    (SELECT song_id FROM list_song WHERE song_list_id  BETWEEN 24 AND 50) s
    ON
    song.id = s.song_id
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

2. 数据库中事务的四大特性(ACID)

如果一个数据库声称支持事务的操作,那么该数据库必须要具备以下四个特性:

  • 原子性(Atomicity)

    原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚,因此事务的操作如果成功就必须要完全应用到数据库,如果操作失败则不能对数据库有任何影响。

  • 一致性(Consistency)

    一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。

    拿转账来说,假设用户A和用户B两者的钱加起来一共是5000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是5000,这就是事务的一致性。

  • 隔离性(Isolation)

    隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。

    即要达到这么一种效果:对于任意两个并发的事务T1和T2,在事务T1看来,T2要么在T1开始之前就已经结束,要么在T1结束之后才开始,这样每个事务都感觉不到有其他事务在并发地执行。

  • 持久性(Durability)

    持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。

    例如我们在使用JDBC操作数据库时,在提交事务方法后,提示用户事务操作完成,当我们程序执行完成直到看到提示后,就可以认定事务以及正确提交,即使这时候数据库出现了问题,也必须要将我们的事务完全执行完成,否则就会造成我们看到提示事务处理完毕,但是数据库因为故障而没有执行事务的重大错误。

    • 脏读

      脏读是指在一个事务处理过程里读取了另一个未提交的事务中的数据。当一个事务正在多次修改某个数据,而在这个事务中这多次的修改都还未提交,这时一个并发的事务来访问该数据,就会造成两个事务得到的数据不一致。例如:用户A向用户B转账100元,对应SQL命令如下

          update account set money=money+100 where name=’B’;  (此时A通知B)
      
          update account set money=money - 100 where name=’A’;
      
      • 1
      • 2
      • 3

      当只执行第一条SQL时,A通知B查看账户,B发现确实钱已到账( 此时即发生了脏读),而之后无论第二条SQL是否执行,只要该事务不提交,则所有操作都将回滚,那么当B以后再次查看账户时就会发现钱其实并没有转。

    • 不可重复读

      不可重复读是指在对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,被另一个事务修改并提交了。

      例如事务T1在读取某一数据,而事务T2立马修改了这个数据并且提交事务给数据库,事务T1再次读取该数据就得到了不同的结果,发送了不可重复读。

      不可重复读和脏读的区别是,脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则是读取了前一事务提交的数据。

      在某些情况下,不可重复读并不是问题,比如我们多次查询某个数据当然以最后查询得到的结果为主。但在另一些情况下就有可能发生问题,例如对于同一个数据A和B依次查询就可能不同,A和B就可能打起来了……

    • 虚读(幻读)

      幻读是事务非独立执行时发生的一种现象。例如事务T1对一个表中所有的行的某个数据项做了从“1”修改为“2”的操作,这时事务T2又对这个表中插入了一行数据项,而这个数据项的数值还是为“1”并且提交给数据库。而操作事务T1的用户如果再查看刚刚修改的数据,会发现还有一行没有修改,其实这行是从事务T2中添加的,就好像产生幻觉一样,这就是发生了幻读。

      幻读和不可重复读都是读取了另一条已经提交的事务(这点就脏读不同),所不同的是不可重复读查询的都是同一个数据项,而幻读针对的是一批数据整体(比如数据的个数)。

3. 现在来看看MySQL数据库为我们提供的四种隔离级别:

  1. **Serializable (串行化):**可避免脏读、不可重复读、幻读的发生。

  2. **Repeatable read (可重复读):**可避免脏读、不可重复读的发生。

  3. **Read committed (读已提交):**可避免脏读的发生。

  4. **Read uncommitted (读未提交):**最低级别,任何情况都无法保证。

    以上四种隔离级别最高的是Serializable级别,最低的是Read uncommitted级别,当然级别越高,执行效率就越低。像Serializable这样的级别,就是以锁表的方式(类似于Java多线程中的锁)使得其他的线程只能在锁外等待,所以平时选用何种隔离级别应该根据实际情况。在MySQL数据库中默认的隔离级别为Repeatable read (可重复读)。

    在MySQL数据库中,支持上面四种隔离级别,默认的为Repeatable read (可重复读);而在Oracle数据库中,只支持Serializable (串行化)级别和Read committed (读已提交)这两种级别,其中默认的为Read committed级别。

4. 索引的优点和缺点

  1. 为什么要创建索引呢(优点)?
    这是因为,创建索引可以大大提高系统的性能。
    第一, 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。
    第二, 可以大大加快数据的检索速度,这也是创建索引的最主要的原因。
    第三, 可以加速表和表之间的连接,特别是在实现数据的参考完整性方面特别有意义。
    第四, 在使用分组和排序子句进行数据检索时,同样可以显著减少查询中分组和排序的时间。
    第五, 通过使用索引,可以在查询的过程中,使用优化隐藏器,提高系统的性能。

  2. 建立方向索引的不利因素(缺点)
    也许会有人要问:增加索引有如此多的优点,为什么不对表中的每一个列创建一个索引呢?这种想法固然有其合理性,然而也有其片面性。虽然,索引有许多优点,但是,为表中的每一个列都增加索引,是非常不明智的。这是因为,增加索引也有许多不利的一个方面。

    第一, 创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加。
    第二, 索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立聚簇索引,那么需要的空间就会更大。
    第三, 当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,这样就降低了数据的维护速度。

5. 查询索引怎么建立的?为什么最左前缀?

  • 1. 索引建立的原则

    用于索引的最好的备选数据列是那些出现在WHERE子句、join子句、ORDER BY或GROUP BY子句中的列。

    仅仅出现在SELECT关键字后面的输出数据列列表中的数据列不是很好的备选列

    SELECT
    col_a <- 不是备选列
    FROM
    tbl1 LEFT JOIN tbl2
    ON tbl1.col_b = tbl2.col_c <- 备选列
    WHERE
    col_d = expr; <- 备选列
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    当然,显示的数据列与WHERE子句中使用的数据列也可能相同。
    我们的观点是输出列表中的数据列本质上不是用于索引的很好的备选列。

  • 2. 复合索引的建立以及最左前缀原则

    索引字符串值的前缀(prefixe)。如果你需要索引一个字符串数据列,那么最好在任何适当的情况下都应该指定前缀长度。
    例如,如果有CHAR(200)数据列,如果前面10个或20个字符都不同,就不要索引整个数据列。
    索引前面10个或20个字符会节省大量的空间
    你可以索引CHAR、VARCHAR、BINARY、VARBINARY、BLOB和TEXT数据列的前缀。

    假设你在表的state、city和zip数据列上建立了复合索引。索引中的数据行按照state/city/zip次序排列,
    因此它们也会自动地按照state/city和state次序排列。这意味着,即使你在查询中只指定了state值,
    或者指定state和city值,MySQL也可以使用这个索引。因此,这个索引可以被用于搜索如下所示的数据列组合:
    state, city, zip
    state, city
    state

    MySQL不能利用这个索引来搜索没有包含在最左前缀的内容。例如,如果你按照city或zip来搜索,
    就不会使用到这个索引。如果你搜索给定的state和具体的ZIP代码(索引的1和3列),
    该索引也是不能用于这种组合值的,尽管MySQL可以利用索引来查找匹配的state从而缩小搜索的范围。

    如果你考虑给已经索引过的表添加索引,那么就要考虑你将增加的索引是否是已有的多列索引的最左前缀。
    如果是这样的,不用增加索引,因为已经有了(例如,如果你在state、city和zip上建立了索引,那么没有必要再增加state的索引)。

6. 数据库设计(微博评论表)

CREATE TABLE `comment`  (
  `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
  `father_id` int(11) NULL DEFAULT NULL,
  `son_id` int(11) NULL DEFAULT NULL,
  `user_id` int(11) NULL DEFAULT NULL,
  `content` varchar(255) NULL DEFAULT NULL,
  `create_time` datetime NOT NULL,
) 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

计算某条微博的总评论数:

返回一条微博下面按照时间排序最近的十条评论:

显示某条评论相关的子评论:

SELECT count(*) FROM comment WHERE id = ?

  • 1
  • 2
SELECT * FROM comment ORDER BY create_time LIMIT 10;
  • 1
SELECT * FROM comment WHERE father_id = ?
  • 1

7. MySQL慢查询

  • 概念
    MySQL的慢查询,全名是慢查询日志,是MySQL提供的一种日志记录,用来记录在MySQL中响应时间超过阀值的语句。

  • 具体环境中,运行时间超过long_query_time值的SQL语句,则会被记录到慢查询日志中。

  • long_query_time的默认值为10,意思是记录运行10秒以上的语句。

  • 默认情况下,MySQL数据库并不启动慢查询日志,需要手动来设置这个参数。

  • 当然,如果不是调优需要的话,一般不建议启动该参数,因为开启慢查询日志会或多或少带来一定的性能影响。

  • 慢查询日志支持将日志记录写入文件和数据库表。

8. 详解MySQL(InnoDB)是如何处理死锁的

  • 什么是死锁

    官方定义如下:两个事务都持有对方需要的锁,并且在等待对方释放,并且双方都不会释放自己的锁。

    这个就好比你有一个人质,对方有一个人质,你们俩去谈判说换人。你让对面放人,对面让你放人。

img

  • 为什么会形成死锁

    看到这里,也许你会有这样的疑问,事务和谈判不一样,为什么事务不能使用完锁之后立马释放呢?居然还要操作完了之后一直持有锁?这就涉及到 MySQL 的并发控制了。

    MySQL的并发控制有两种方式,一个是 MVCC,一个是两阶段锁协议。那么为什么要并发控制呢?是因为多个用户同时操作 MySQL 的时候,为了提高并发性能并且要求如同多个用户的请求过来之后如同串行执行的一样(可串行化调度)。具体的并发控制这里不再展开。咱们继续深入讨论两阶段锁协议。

    • 两阶段锁协议(2PL)

      官方定义:

      两阶段锁协议是指所有事务必须分两个阶段对数据加锁和解锁,在对任何数据进行读、写操作之前,事务首先要获得对该数据的封锁;在释放一个封锁之后,事务不再申请和获得任何其他封锁。

      对应到 MySQL 上分为两个阶段:

      1. 扩展阶段(事务开始后,commit 之前):获取锁
      2. 收缩阶段(commit 之后):释放锁

      就是说呢,只有遵循两段锁协议,才能实现 可串行化调度。

      但是两阶段锁协议不要求事务必须一次将所有需要使用的数据加锁,并且在加锁阶段没有顺序要求,所以这种并发控制方式会形成死锁。

  • MySQL 如何处理死锁?

    MySQL有两种死锁处理方式:

    1. 等待,直到超时(innodb_lock_wait_timeout=50s)。
    2. 发起死锁检测,主动回滚一条事务,让其他事务继续执行(innodb_deadlock_detect=on)。

    由于性能原因,一般都是使用死锁检测来进行处理死锁。

    • 死锁检测

      死锁检测的原理是构建一个以事务为顶点、锁为边的有向图,判断有向图是否存在环,存在即有死锁。

    • 回滚

      检测到死锁之后,选择插入更新或者删除的行数最少的事务回滚,基于 INFORMATION_SCHEMA.INNODB_TRX 表中的 trx_weight 字段来判断。

  • 如何避免发生死锁

    • 收集死锁信息:

      1. 利用命令 SHOW ENGINE INNODB STATUS查看死锁原因。
      2. 调试阶段开启 innodb_print_all_deadlocks,收集所有死锁日志。
    • 减少死锁:

      1. 使用事务,不使用 lock tables 。
      2. 保证没有长事务。
      3. 操作完之后立即提交事务,特别是在交互式命令行中。
      4. 如果在用 (SELECT … FOR UPDATE or SELECT … LOCK IN SHARE MODE),尝试降低隔离级别。
      5. 修改多个表或者多个行的时候,将修改的顺序保持一致。
      6. 创建索引,可以使创建的锁更少。
      7. 最好不要用 (SELECT … FOR UPDATE or SELECT … LOCK IN SHARE MODE)。
      8. 如果上述都无法解决问题,那么尝试使用 lock tables t1, t2, t3 锁多张表

六、Spring

1. 简单介绍Spring是什么?

1、Spring的核心是一个轻量级(Lightweight)的容器(Container)。
2、Spring是实现IoC(Inversion of Control)容器和非入侵性(No intrusive)的框架。
3、Spring提供AOP(Aspect-oriented programming)概念的实现方式。
4、Spring提供对持久层(Persistence)、事物(Transcation)的支持。
5、Spring供MVC Web框架的实现,并对一些常用的企业服务API(Application Interface)提供一致的模型封装。
6、Spring提供了对现存的各种框架(Structs、JSF、Hibernate、Ibatis、Webwork等)相整合的方案。
总之,Spring是一个全方位的应用程序框架。

spring 的优点?

1.降低了组件之间的耦合性 ,实现了软件各层之间的解耦

2.可以使用容易提供的众多服务,如事务管理,消息服务等

3.容器提供单例模式支持

4.容器提供了AOP技术,利用它很容易实现如权限拦截,运行期监控等功能

5.容器提供了众多的辅助类,能加快应用的开发

6.spring对于主流的应用框架提供了集成支持,如hibernate,JPA,Struts等

7.spring属于低侵入式设计,代码的污染极低

8.独立于各种应用服务器

9.spring的DI机制降低了业务对象替换的复杂性

10.Spring的高度开放性,并不强制应用完全依赖于Spring,开发者可以自由选择spring的部分或全部

2. Spring MVC

MVC框架

M: Model模型:和数据库进行交互

V: View,视图: 产生html页面

C: Controller,控制器: 接收请求,进行处理,与M和V进行交互,返回应答

MVC 是 Model、View 和 Controller 的缩写,分别代表 Web 应用程序中的 3 种职责。

  • 模型:用于存储数据以及处理用户请求的业务逻辑。
  • 视图:向控制器提交数据,显示模型中的数据。
  • 控制器:根据视图提出的请求判断将请求和数据交给哪个模型处理,将处理后的有关结果交给哪个视图更新显示。

基于 Servlet 的 MVC 模式的具体实现如下。

  • 模型:一个或多个 JavaBean 对象,用于存储数据(实体模型,由 JavaBean 类创建)和处理业务逻辑(业务模型,由一般的 Java 类创建)。
  • 视图:一个或多个 JSP 页面,向控制器提交数据和为模型提供数据显示,JSP 页面主要使用 HTML 标记和 JavaBean 标记来显示数据。
  • 控制器:一个或多个 Servlet 对象,根据视图提交的请求进行控制,即将请求转发给处理业务逻辑的 JavaBean,并将处理结果存放到实体模型

3. Spring Bean的作用域和生命周期

一、Bean的作用域

在Bean容器启动会读取bean的xml配置文件,然后将xml中每个bean元素分别转换成BeanDefinition对象。在BeanDefinition对象中有scope 属性,就是它控制着bean的作用域。
Spring框架支持5种作用域,有三种作用域是当开发者使用基于web的ApplicationContext的时候才生效的。下面就是Spring直接支持的作用域了,当然开发者也可以自己定制作用域。

作用域描述
单例(singleton)(默认)每一个Spring IoC容器都拥有唯一的一个实例对象
原型(prototype)一个Bean定义,任意多个对象
请求(request)一个HTTP请求会产生一个Bean对象,也就是说,每一个HTTP请求都有自己的Bean实例。只在基于web的Spring ApplicationContext中可用
会话(session)限定一个Bean的作用域为HTTPsession的生命周期。同样,只有基于web的Spring ApplicationContext才能使用
全局会话(global session)限定一个Bean的作用域为全局HTTPSession的生命周期。通常用于门户网站场景,同样,只有基于web的Spring ApplicationContext可用

二、Bean的生命周期

前面章节介绍了bean容器以及bean的配置与注入,本章节学习bean的生命周期,了解bean是怎么来的又是怎么没的。

img

ApplicationContext容器中,Bean的生命周期流程如上图所示,流程大致如下:

  1. 首先容器启动后,会对scope为singleton且非懒加载的bean进行实例化,
  2. 按照Bean定义信息配置信息,注入所有的属性,
  3. 如果Bean实现了BeanNameAware接口,会回调该接口的setBeanName()方法,传入该Bean的id,此时该Bean就获得了自己在配置文件中的id,
  4. 如果Bean实现了BeanFactoryAware接口,会回调该接口的setBeanFactory()方法,传入该Bean的BeanFactory,这样该Bean就获得了自己所在的BeanFactory,
  5. 如果Bean实现了ApplicationContextAware接口,会回调该接口的setApplicationContext()方法,传入该Bean的ApplicationContext,这样该Bean就获得了自己所在的ApplicationContext,
  6. 如果有Bean实现了BeanPostProcessor接口,则会回调该接口的postProcessBeforeInitialzation()方法,
  7. 如果Bean实现了InitializingBean接口,则会回调该接口的afterPropertiesSet()方法,
  8. 如果Bean配置了init-method方法,则会执行init-method配置的方法,
  9. 如果有Bean实现了BeanPostProcessor接口,则会回调该接口的postProcessAfterInitialization()方法,
  10. 经过流程9之后,就可以正式使用该Bean了,对于scope为singleton的Bean,Spring的ioc容器中会缓存一份该bean的实例,而对于scope为prototype的Bean,每次被调用都会new一个新的对象,期生命周期就交给调用方管理了,不再是Spring容器进行管理了
  11. 容器关闭后,如果Bean实现了DisposableBean接口,则会回调该接口的destroy()方法,
  12. 如果Bean配置了destroy-method方法,则会执行destroy-method配置的方法,至此,整个Bean的生命周期结束。

这里在UserBean类基础上进行改造,增加了name属性并实现了ApplicationContextAware接口。

4. Springboot 注解

@PathVariable :

  • 通过 @PathVariable 可以将 URL 中占位符参数绑定到控制器处理方法的入参中:URL 中的 {xxx} 占位符可以通过@PathVariable(“xxx“) 绑定到操作方法的入参中。

@RequestBody和@RequestParam

  • 两个注解都是用于方法中接收参数使用的,两者也有一定的区别。
  • @RequestBody这个一般处理的是在ajax请求中声明contentType: “application/json; charset=utf-8”时候。也就是json数据或者xml(我没用过这个,用的是json)
  • @RequestParam这个一般就是在ajax里面没有声明contentType的时候,为默认的。。。urlencode格式时,用这个。
  • @RequestBody可以直接将页面中的参数封装成实体类中的数据传输给后端

@PostMapping @GetMapping @RequestMapping

  • @GetMapping是一个组合注解,是@RequestMapping(method = RequestMethod.GET)的缩写。
  • @PostMapping是一个组合注解,是@RequestMapping(method = RequestMethod.POST)的缩写。
  • @RequestMapping是一个非 组合注解,需要自定义请求方式。

@RestController和@Controller

  • RestController相当于Controller+ResponseBody注解
    如果只是使用@RestController注解Controller,则Controller中的方法无法返回jsp页面,或者html,配置的视图解析器 ,也就是相当于在方法上面自动加了ResponseBody注解,所以没办法跳转并传输数据到另一个页面,所以InternalResourceViewResolver也不起作用,返回的内容就是Return 里的内容,即数据直接甩在当前请求的页面上,适用于ajax异步请求。

5. Springboot各个层之间的联系

Springboot框架分controller层,service层和dao层,分别负责不同的业务。

  • Controller层沟通前后端,注解为@RestController
  • Service层沟通DAO层和Ccontroller层,注解为@Service
  • DAO层沟通数据库和service层,注解为@Repository

View层声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家小花儿/article/detail/334317?site

推荐阅读
相关标签