什么是B/S架构,C/S架构?Java都有那些开发平台?
B/S(Browser/Server),浏览器/服务器程序
C/S(Client/Server),客户端/服务端,桌面应用程序
JAVA SE:主要用在客户端开发
JAVA EE:主要用在web应用程序开发
JAVA ME:主要用在嵌入式应用程序开发
JDK, JRE, JVM?JDK(Java Development Kit): JDK是Java开发工具包,它是Java开发的完整工具集,包括了Java编译器(javac)、Java虚拟机(JVM)、Java类库等。主要用于Java应用程序的开发,提供了开发、编译、调试和运行Java程序所需的工具。JRE(Java Runtime Environment): JRE是Java运行时环境,它是Java应用程序执行的环境,包含了Java虚拟机(JVM)和Java类库。用于在计算机上运行已经编译过的Java应用程序,但不包含用于Java开发的工具,如编译器。JVM(Java Virtual Machine): JVM是Java虚拟机,是一个在物理计算机上模拟运行Java字节码(.class)的虚拟机。负责解释和执行Java字节码,通过不同操作系统上的 JVM 解释为该操作系统的机器指令,使得Java程序能够在不同的平台上实现一次编译,到处运行的跨平台特性。
Java语言有哪些特点?
简单易学、有丰富的类库
面向对象 OOP(Java最重要的特性,让程序耦合度更低,内聚性更高) 类是对象的抽象,对象是类的具体,类是对象的模板,对象是类的实例
与平台无关性(JVM是Java跨平台使用的根本)
可靠安全
支持多线程
一个java类中包含那些内容? 属性、方法、内部类、构造方法、代码块。
数据结构?Java的数据结构有那些?Java中有几种数据类型? 数据结构:计算机保存,组织数据的方式 java中数据结构有:1.线性表 2.链表 3.栈 4.队列 5.图 6.树 数据类型有,整形:byte,short,int,long;浮点型:float,double;字符型:char;布尔型:boolean
float 和 double. 两种用于表示浮点数的数据类型。它们之间的主要区别在于精度和存储大小。
float是一种单精度浮点数数据类型,通常用于需要小数点的计算,但它不如double类型精确。float类型有7位十进制有效数字,并且它的存储大小是32位(4字节)。
double是一种双精度浮点数数据类型,比float类型有更高的精度,通常用于需要更精确计算的情况。double类型有15位十进制有效数字,并且它的存储大小是64位(8字节)。 1 double myDouble = 3.141592653589793 ;
如何解决浮点型数据运算出现的误差的问题?float f=3.4;是否正确? 使用 Bigdecimal类
进行浮点型数据的运算。 3.4 是双精度数(double),将 double 赋值给 float 属于下转型(down-casting,也称为窄化)会造成精度损失,因此需要强制类型转换 float f =(float)3.4; 或 float f =3.4F
short s1 = 1; s1 = s1 + 1;有错吗?short s1 = 1; s1 += 1; 有错吗? 对于 short s1 = 1; s1 = s1 + 1;由于 1 是 int 类型,因此 s1+1 运算结果也是 int 型,需要强制转换类型才能赋值给 short 型。 而 short s1 =1; s1 += 1;可正确编译,因为 s1+= 1;相当于 s1 = (short)(s1 + 1);其中有隐含的强制类型转换。
int i = 0; count = (i++)+ (i++)+ (i++); 值为? count = 0 + 1 + 2 = 3
什么是隐式转换,什么是显式转换?Char类型能不能转成int类型,string类型,double类型? 显示转换就是类型强转,把一个大类型的数据强制赋值给小类型的数据;隐式转换就是大范围的变量能够接受小范围的数据;隐式转换和显式转换其实就是自动类型转换和强制类型转换。 Char < int < long < float < double;Char类型可以隐式转成int,double类型,但是不能隐式转换成string;如果char类型转成byte,short类型的时候,需要强转。
char 型变量中能不能存贮一个中文汉字? 可以,因为 Java 中使用的编码是 Unicode(不选择任何特定的编码,直接使用字符在字符集中的编号,这是统一的唯一方法),一个 char 类型占 2 个字节(16 比特),所以放一个中文是没问题的。 补充:使用 Unicode 意味着字符在 JVM 内部和外部有不同的表现形式,在 JVM内部都是 Unicode,当这个字符被从 JVM 内部转移到外部时(例如存入文件系统中),需要进行编码转换。所以 Java 中有字节流和字符流,以及在字符流和字节流之间进行转换的转换流,如InputStreamReader 和 OutputStreamReader,这两个类是字节流和字符流之间的适配器类,承担了编码转换的任务;对于 C 程序员来说,要完成这样的编码转换恐怕要依赖于 union(联合体/共用体)共享内存的特征来实现了。
数组在java中是一种基本数据类型吗?引用类型? 在Java中,数组(Array)是一种引用类型,而不是基本数据类型。例如,如果你创建数组:int[] numbers = new int[5]; 这里,numbers是一个引用变量,指向一个包含5个int类型元素的数组对象。这个数组对象本身不是基本数据类型,而是引用类型。数组中的每个元素(在这种情况下是int类型)是基本数据类型。
什么是基本数据类型和引用类型?
基本数据类型:是编程语言中内置的简单数据类型,用于直接存储具体的值。这些值通常是不可变的,即一旦赋值,就不能改变它们的类型。基本数据类型通常包括整形:byte,short,int,long;浮点型:float,double;字符型:char;布尔型:boolean。在Java中,基本数据类型是直接存储值的,它们占用的内存空间是固定的,且不会引用其他对象。
引用类型:与基本数据类型相对,它们不直接存储值,而是存储对内存中对象的引用。这意味着,引用类型的变量实际上是一个指针,指向存储在堆内存中的对象。当你创建一个引用类型的变量(如数组、类、接口等)时,你实际上是在内存中创建了一个对象,并将变量设置为指向这个对象的引用。引用类型包括类(Class)、接口(Interface)、数组(Array)等。
Java 中变量是“值传递” 还是 “引用传递”? 在Java中,所有的变量都是值传递,无论是基本数据类型还是引用类型。
基本数据类型的变量存储的是值本身。当你将一个基本数据类型的值赋给另一个变量时,实际上是复制了这个值。所以,对于基本数据类型,Java确实是通过值来传递的。 当一个基本类型变量作为方法的参数传递时,传递的是该变量的一个副本。这意味着在方法内部对该副本所做的任何修改都不会影响到原始变量。基本类型变量是存储在栈内存中的,并且它们的值是直接存储的,而不是通过引用。 1 2 3 4 5 6 7 8 9 10 11 public class Main { public static void main (String[] args) { int originalValue = 5 ; modifyValue(originalValue); System.out.println(originalValue); } public static void modifyValue (int value) { value = 10 ; System.out.println(value); } }
一旦一个基本类型变量被初始化并存储在栈内存中,你不能直接修改它存储在栈内存中的值。基本类型变量是不可变的(java所有基本类型都不可变?!!),一旦它们被赋值,它们的值就不能被改变。如果想修改其值,必须重新赋一个新的值。这通常是通过声明一个新的变量,或者重新赋值给原来的变量来完成的。 1 2 3 4 5 6 7 8 9 public class Main { public static void main (String[] args) { int number = 5 ; System.out.println("Before modification: " + number); number = 10 ; System.out.println("After modification: " + number); } }
引用类型的变量实际上存储的是一个指向内存中对象的引用的地址。当你将一个引用类型的变量赋给另一个变量时,你实际上是在复制这个引用地址,而不是对象本身。这意味着两个变量现在指向同一个对象。因此,尽管在引用类型中,传递的是引用(地址),但这并不等同于传统意义上的“引用传递”。 在Java中,对象本身是通过堆内存来存储的,而引用变量(即对象的引用)是通过栈内存来存储的。当你将一个引用类型的变量赋值给另一个变量时,你实际上是在栈内存中复制了这个引用地址,而不是复制了整个对象。
Java创建对象有几种方式?
使用 new
关键字: 1 MyClass obj = new MyClass ();
通过反射机制: 1 2 3 4 5 6 try { Class<?> clazz = Class.forName("MyClass" ); MyClass obj = (MyClass) clazz.newInstance(); } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) { e.printStackTrace(); }
使用 clone()
方法: 1 2 3 4 5 6 MyClass original = new MyClass ();try { MyClass cloned = (MyClass) original.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); }
通过反序列化: 1 2 3 4 5 6 7 8 ObjectOutputStream out = new ObjectOutputStream (new FileOutputStream ("object.ser" ));out.writeObject(new MyClass ()); out.close(); ObjectInputStream in = new ObjectInputStream (new FileInputStream ("object.ser" ));MyClass obj = (MyClass) in.readObject();in.close();
使用匿名类
工厂方法
使用静态工厂方法
面向对象和面向过程的区别。
面向过程:一种较早的编程思想,顾名思义就是该思想是站着过程的角度思考问题,强调的就是功能行为,功能的执行过程,即先后顺序,而每一个功能我们都使用函数(类似于方法)把这些步骤一步一步实现。使用的时候依次调用函数就可以了。
面向对象:一种基于面向过程的新编程思想,顾名思义就是该思想是站在对象的角度思考问题,我们把多个功能合理放到不同对象里,强调的是具备某些功能的对象。 具备某种功能的实体,称为对象。面向对象最小的程序单元是类。面向对象更加符合常规的思维方式,稳定性好,可重用性强,易于开发大型软件产品,有良好的可维护性。 在软件工程上,面向对象可以使工程更加模块化,实现更低的耦合和更高的内聚。
instanceof 关键字的作用 instanceof 严格来说是Java中的一个双目运算符,用来测试一个对象是否为一个类的实例,用法为: 1 boolean result = obj instanceof Class
其中 obj 为一个对象,Class 表示一个类或者一个接口,当 obj 为 Class 的对象,或者是其直接或间接子类,或者是其接口的实现类,结果result 都返回 true,否则返回false。注意:编译器会检查 obj 是否能转换成右边的class类型,如果不能转换则直接报错,如果不能确定类型,则通过编译,具体看运行时定。
封装 ?什么是拆装箱? Java面向对象语言,一切操作以对象为基础。对象中封装了属性和操作,使用灵活,数据不被外部修改。封装类在处理集合、泛型、反射等场景中非常有用。 装箱就是自动将基本数据类型转换为包装器类型(int->Integer);调用方法:Integer的valueOf(int) 方法拆箱就是自动将包装器类型转换为基本数据类型(Integer->int)。
int 与 Integer初始值? Integer初始值为null,存储在堆内存;int初始值0,存储在栈空间。
抽象 抽象是将一类对象的共同特征总结出来构造类的过程, 包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。
面向对象的特征有哪些方面?
封装:通常认为封装是把数据和操作数据的方法绑定起来,对数据的访问只能通过已定义的接口。
面向对象的本质就是将现实世界描绘成一系列完全自治、封闭的对象。我们在类中编写的方法就是对实现细节的一种封装;编写一个类就是对数据和数据操作的封装。
优点:
信息隐藏 : 允许隐藏对象的内部状态和实现细节,只暴露必要的接口。这样可以保护对象的内部数据不被外部直接访问和修改,减少错误和不一致的状态。
模块化 : 将复杂的系统分解为独立的、可管理的模块。每个模块负责特定的功能,使得开发和维护更加简单。
易于维护 : 封装使得对象的实现可以独立于其使用者进行更改。只要接口保持不变,内部的修改不会影响到使用该对象的其他部分代码。
继承:继承是从已有类得到继承信息创建新类的过程.
提供继承信息的类被称为父类(超类、基类);得到继承信息的类被称为子类(派生类)。继承让变化中的软件系统有了一定的延续性,同时继承也是封装程序中可变因素的重要手段。
优点:
代码重用 : 继承允许子类继承父类的属性和方法,减少了代码的重复编写,提高了开发效率。
建立层次结构 : 继承支持创建一个类的层次结构,这有助于组织和管理具有共同特征的对象。子类可以扩展或覆盖父类的行为,从而实现更具体的功能。
多态的基础 : 继承是多态实现的基础。通过继承,可使用父类类型的引用来操作子类对象,为多态提供了可能。
多态性:是指允许不同子类型的对象对同一消息作出不同的响应。
简单的说就是用同样的对象引用调用同样的方法但是做了不同的事情。多态性分为编译时的多态性和运行时的多态性。
优点:
接口统一 : 允许不同的子类对象通过统一的接口进行操作,简化了客户端代码的复杂性,可以处理不同类型的对象而不需要知道对象的具体类型。
扩展性 : 提高了代码的扩展性。如果需要引入新的子类,只需确保它遵循现有的接口,而无需修改已有的代码。
动态绑定 : 在Java等语言中,多态允许在运行时动态决定对象的具体类型和调用的方法。这种动态绑定提供了更大的灵活性和更强的表达能力。
方法重载(overload)实现的是编译时的多态性(也称为前绑定),编译器在编译时根据参数类型和数量确定哪个方法 1 2 public int add (int a, int b) return a + b; public double add (double a, double b) return a + b;
方法重写(override)实现的是运行时的多态性(也称为后绑定)。运行时的多态是面向对象最精髓的东西,要实现多态需要做两件事: 1、方法重写(子类继承父类并重写父类中已有的或抽象的方法); 2、对象造型(用父类型引用引用子类型对象,这样同样的引用调用同样的方法就会根据子类对象的不同而表现出不同的行为)。 运行时的多态性主要通过方法重写和对象的多态性实现,运行时根据对象的实际类型来调用相应的方法。这意味着在运行时才能确定调用哪个方法,这取决于对象的实际类型。编译时,Java编译器只知道引用变量的类型,而不知道它所引用的对象的实际类型。因此,方法调用的确定(即分派)被推迟到运行时。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Animal { public void sound () { System.out.println("The animal makes a sound" ); } } class Dog extends Animal { @Override public void sound () { System.out.println("The dog barks" ); } } public class Main { public static void main (String[] args) { Animal animal = new Dog (); animal.sound(); } }
总的来说,封装、继承和多态共同为面向对象编程提供了一种强大的方式来构建复杂和可扩展的软件系统。它们使得代码更加清晰、易于维护和扩展,同时也提高了代码的复用性和可读性。极大地提高了软件开发的效率、可维护性和可扩展性。
重载和重写的区别
重写 Override:在子类中把父类本身有的方法重新写一遍。子类继承了父类原有的方法,但有时子类并不想原封不动的继承父类中的某个方法,所以在方法名,参数列表,返回类型(除过子类中方法的返回值是父类中方法返回值的子类时)都相同的情况下, 对方法体进行修改或重写,这就是重写。被重写方法比父类更好访问(即子类函数的访问修饰权限不能少于父类的),不能比父类被重写方法声明更多的异常(里氏代换原则)。 1、发生在父类与子类之间 2、方法名,参数列表,返回类型(除过子类中方法的返回类型是父类中返回类型的子类)必须相同 3、访问修饰符的限制一定要大于被重写方法的访问修饰符(public>protected>default>private) ,子类重写方法应该要比父类方法具有相同或更广泛的可见性,子类对外暴露的接口比父类更多,这符合面向对象编程中的开闭原则(对扩展开放,对修改关闭),提供了更多的灵活性。 4、重写方法一定不能抛出新的检查异常或者比被重写方法申明更加宽泛的检查型异常
重载(Overload)在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同甚至是参数顺序不同)则视为重载。同时,重载对返回类型没有要求,可以相同也可以不同,但不能通过返回类型是否相同来判断重载。 1、重载Overload是一个类中多态性的一种表现 2、重载要求同名方法的参数列表不同(参数类型,参数个数甚至是参数顺序) 3、重载的时候,返回值类型可以相同也可以不相同。无法以返回型别作为重载函数的区分标准
动态绑定? 是指在运行时确定方法调用的具体实现。它是 Java 中的一种多态性表现,通过动态绑定,程序可以根据对象的实际类型来决定调用哪个方法实现。 动态绑定的实现依赖于 Java 中的继承和重写机制。当子类重写父类的方法时,如果父类引用指向子类对象,并且调用被重写的方法时,将根据实际对象类型决定调用哪个版本的方法,这就是动态绑定。使得 Java 具有更强大的多态性,可以根据对象的实际类型来调用不同的方法实现,增强了程序的灵活性和可扩展性。
public,private,protected,以及不写(默认)时的区别? 在Java中,访问修饰符用于指定类、变量、方法和构造函数的访问权限。它们有四种级别:public、private、protected和默认(无修饰符)。这些修饰符决定了哪些其他类可以访问特定的成员。Java 中,外部类的修饰符只能是 public 或默认,类的成员(包括内部类)的 修饰符可以是以上四种。
public声明类的成员时,它可以在任何其他类中(同一包中/不同的包中)被访问。
private成员只能在其自己的类中被访问。它不能被任何其他类(即使是同一包中的类)访问。
protected成员可以在其自己的类、同一包中的其他类以及任何子类(无论子类在哪里)中被访问。 它提供了一种有限的访问,比private更开放,但仍然限制了对类的直接访问。 这在创建API或库时特别有用,因为你可能希望允许子类访问某些方法或变量,但不希望它们被其他不相关的类访问。
默认(无修饰符)default:当一个类的成员没有明确的访问修饰符时,它只能在同一包中的其他类中被访问。对于同一个包中的其他类相当于 public,对于不是同一个包中的其他类相当于 private。这意味着它不能被其他包中的类直接访问,但可以被同一包中的任何类访问。这是比private更开放,但比protected和public更受限的访问级别。
String 是最基本的数据类型吗? 不是。Java 中的基本数据类型只有 8 个;除了基本类型 剩下的都是引用类型,Java 5 以后引入的枚举类型也算是一种比较特殊的引用类型。 String 是不可变类。不可变类指的是无法修改对象的值,当你创建一个 String 对象之后,这个对象就无法被修改。像执行s += “a”; 返回的是一个新的 String 对象,老的 s 指向的对象不会发生变化,只是 s 的引用指向了新的对象而已。“不可变”最主要的好处就是安全,在多线程环境下也是线程安全的;然后,配合常量池可以节省内存空间,且获取效率也更高(如果常量池里面已经有这个字符串对象了,就不需要新建,直接返回即可)。
String的初始值是?NULL.
String 常见题.
“字面量创建字符串”:yesA 是一个引用指向了堆里面的字符串常量池里的对象 a。如果字符串常量池已经有了 abb,那么直接返回其引用,如果没有 abb,则会创建 abb 对象,然后返回其引用。 1 2 3 String yesA = "abb" ; String yesB = "abb" ; System.out.printIn(yesA == yesB);
“new String创建字符串”:先判断字符串常量池里面是否有 abb,如果没有 abb 则创建一个 abb,然后会在堆内存里面创建一个对象 abb,返回堆内存对象 abb 的引用,也就是说返回的不是字符串常量池里面的 abb 1 2 3 yesA = new String ("abb" ); yesB = new String ("abb" ); System.out.println(yesA == yesB);
intern():判断下 yesB 引用指向的值在字符串常量里面是否有,如果没有就在字符串常量池里面新建一个 aaabbb 对象,返回其引用,如果有则直接返回引用。 1 2 3 4 5 String yesA = "aaabbb" ; String yesB = new String ("aaa" ) + new String ("bbb" ); String yesC = yesB.intern(); System.out.println(yesA == yesB); System.out.println(yesA == yesC);
JDK 1.6 时,字符串常量池是放置在永久代的; ??? JDK 1.7 之后字符串常量池是放在堆内的 1 2 3 4 5 String yesB = new String ("aaa" ) + new String ("bbb" ); String yesC = yesB.intern(); String yesA = "aaabbb" ; System.out.println(yesA == yesB); System.out.println(yesA == yesC);
String 连接?
使用 +
运算符:字符串连接最简单的方法,但在大量连接操作时可能效率较低,因为它会生成多个临时的字符串对象。
使用 StringBuilder
:可变的字符序列,适用于需要频繁进行字符串连接的场景。append
方法用于添加字符串内容,最后使用 toString
方法获取最终的字符串。适合单线程环境下使用。
使用 StringBuffer
:与 StringBuilder
类似,也是可变的字符序列,但不同之处在于 StringBuffer
是线程安全的,适用于多线程环境。
String,StringBuffer 和 StringBuilder 的区别是什么? String是只读字符串,它并不是基本数据类型,而是一个对象。从底层源码来看是一个final类型的字符数组,所引用的字符串不能被改变,一经定义,无法再增删改。每次对String的操作都会生成新的String对象。 StringBuffer与StringBuilder都继承了AbstractStringBulder类,而AbtractStringBuilder又实现了CharSequence接口,两个类都是用来进行字符串操作的。在做字符串拼接修改删除替换时,效率比string更高。 StringBuffer是线程安全的,Stringbuilder是非线程安全的。所以Stringbuilder比stringbuffer效率更高,StringBuffer的方法大多都加了synchronized关键字
String类的常用方法有那些? charAt:返回指定索引处的字符 indexOf():返回指定字符的索引 replace():字符串替换 trim():去除字符串两端空白 split():分割字符串,返回一个分割后的字符串数组 getBytes():返回字符串的byte类型数组 length():返回字符串长度 toLowerCase():将字符串转成小写字母 toUpperCase():将字符串转成大写字符 substring():截取字符串 format():格式化字符串 equals():字符串比较
分割字符串常用的方法。
split(String regex)
方法: 使用正则表达式来分割字符串。返回一个字符串数组,包含分割后的子字符串。 1 2 3 String input = "apple,orange,banana" ;String[] fruits = input.split("," );
split
方法配合正则表达式的转义字符: 如果分隔符是正则表达式的元字符,需要进行转义。 1 2 3 String input = "apple.orange.banana" ;String[] fruits = input.split("\\." );
substring
和 indexOf
方法: 手动截取子字符串。 1 2 3 4 5 String input = "apple,orange,banana" ;int commaIndex = input.indexOf("," );String firstPart = input.substring(0 , commaIndex);String secondPart = input.substring(commaIndex + 1 );
使用 Pattern
和 Matcher
类: 进行更复杂的正则表达式匹配和分割。 1 2 3 4 5 6 7 import java.util.regex.Pattern;import java.util.regex.Matcher;String input = "apple,orange;banana" ;Pattern pattern = Pattern.compile("[,;]" );String[] fruits = pattern.split(input);
JDK9为什么要将 String 的底层实现由 char[] 改为 byte[]? jdk中字符用utf-16编码(UTF-16是Unicode的一种实现方式,它使用16位的编码单元来表示一个字符),一个字符char要占用2个字节;但是对于由纯英文字符和ascii字符组成的字符串,只需要一个字节就可以表示所有ascii字符,使用 byte[] 可以节省一半空间。 只有在需要存储非ascii字符时,才会使用char[]
Unicode 和 UTF-8 的区别:
Unicode(统一码): Unicode 是一种字符集(Character Set),用于定义字符和字符编码之间的对应关系。 Unicode 中包含了世界上几乎所有的字符,包括各种语言的文字、符号、表情符号等。 Unicode 采用 16 位(2 字节)来表示一个字符,因此可以表示的字符范围很广泛,共有 65536 个码位(Code Point)。
UTF-8(Unicode Transformation Format-8): UTF-8 是一种变长的字符编码方式,用于将 Unicode 字符编码成字节序列。 UTF-8 的最大特点是兼容 ASCII 码,即 ASCII 码中的字符(0-127)与 UTF-8 中的编码是相同的,这使得 UTF-8 在 Web 上具有广泛的应用。 UTF-8 使用 1 到 4 个字节来表示一个 Unicode 字符,根据字符的不同而变化,因此可以灵活地表示各种字符,包括中文、日文、韩文等等。
++i与i++的区别
i++:先赋值,后计算;
++i:先计算,后赋值
在JVM层面,这两个操作的实现是通过指令集中的不同指令来完成的。Java虚拟机中的字节码指令包含 iinc 指令用于递增局部变量的值。这两种递增操作在底层都是通过 iinc 指令实现的,但在具体的使用上有一些差异。 1 2 3 4 getstatic i iconst_1 iadd putstatic i
1 2 3 4 getstatic i iconst_1 isub putstatic i
count += –count; 值为? 1、 --count
表示先对 count
执行减一操作,然后将结果赋给 count
。 2、 然后将 count
的当前值(执行了减一操作后的值)加上 count
的当前值,再将结果赋给 count
。
a=a+b 与 a+=b 有什么区别吗?
+= 操作符会进行隐式自动类型转换,此处a+=b隐式的将加操作的结果类型强制转换为持有结果的类型, 而a=a+b则不会自动进行类型转换.如: 1 2 3 4 5 6 byte a = 127 , b = 127 ;b = a + b; b += a; short s1= 1 ;s1 = s1 + 1 ; s1 += 1 ;
&和&&的区别 &是位运算符。&&是布尔逻辑运算符 在进行逻辑判断时用&处理的前面为false后面的内容仍需处理,用&&处理的前面为false不再处理后面的内容。
200 + null 值为? Java 中,如果对一个整数和 null 值进行加法运算,会导致编译错误。在运行时,如果存在 null 值参与运算,会抛出 NullPointerException 异常。
(-10) % (-3) = ?
如果被取模数为正数,结果的符号与被取模数相同。10 = 3 * 3 + 1;
如果被取模数为负数,结果的符号与除数相同。 (-10) = (-3) * 3 + (-1); 所以 (-10) % (-3)
= -1。
Java 常用的类,包,接口。 类:BufferedReader BufferedWriter FileReader FileWirter String Integer 常用的包:java.lang java.awt java.io java.util java.sql Java.net Java.math 常用的接口:Remote List Map Document NodeList
Object类常用方法有那些? Equals Hashcode toString wait notify clone getClass
equals与==的区别
==:比较的是变量(栈)内存中存放的对象的(堆)内存地址,用来判断两个对象的地址是否相同,即是否是指相同一个对象。比较的是真正意义上的指针操作。 1、比较的是操作符两端的操作数是否是同一个对象。 2、两边的操作数必须是同一类型的(可以是父子类之间)才能编译通过。 3、比较基本数据类型的 ==
操作符直接比较它们的值,值相等则为true。如:int a=10 与 long b=10L 与 double c=10.0都是相同的(为true),因为他们都指向地址为10的堆。而对于 Integer a =10 与 Long b = 10L, 使用 ==
比较的是对象的引用而不是值,结果为 false。
equals:用来比较的是两个对象的内容是否相等,由于所有的类都是继承自java.lang.Object类的,所以适用于所有对象,如果没有对该方法进行覆盖的话,调用的仍然是Object类中的方法,而Object中的equals方法返回的却是==的判断。
equals()
和 hashCode()
方法。为什么重写 equals()
时通常也需要重写 hashCode()
?
equals()
方法 :用于比较两个对象是否在逻辑上相等。默认实现是比较对象的内存地址(相当于 ==),即两个对象是否是同一个对象。在实际应用中,一般需要根据对象的业务含义重写该方法,比较对象的实际内容。
hashCode()
方法: 用于获取对象的哈希码,返回一个整数。哈希码是一种用于快速查找的技术,通常在集合(如 HashMap、HashSet)中用到。它可以帮助确定对象在哈希表中的存储位置,提高查找的效率。默认实现是c++编写的native方法,基于对象的内存地址生成哈希码。在实际应用中,一般需要在类中重写该方法,以便相等的对象具有相同的哈希码。 但是,不是同一个对象,使用hashCode()返回的int值(取值范围2^32)也可能相等,即发生了hash冲突。
关系: 在使用哈希表的集合中,hashCode()
和 equals()
之间存在一定的关系。如果两个对象通过 equals()
方法比较相等,它们的 hashCode()
应该返回相同的值。这是为了保持一致性,使得相等的对象在哈希表中能够正确地识别和处理。确保相等的对象具有相同的哈希码,从而使得在集合中正确地处理相等性。 如果两个对象通过 equals()
方法比较相等,但它们的 hashCode()
不相等,那么当放入哈希表等集合中时,它们将被视为不同的对象。这可能导致哈希表中存在相等的对象,破坏了集合的一致性。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class MyClass { private int id; private String name; @Override public boolean equals (Object obj) { if (this == obj) return true ; if (obj == null || getClass() != obj.getClass()) return false ; MyClass myClass = (MyClass) obj; return id == myClass.id && Objects.equals(name, myClass.name); } @Override public int hashCode () { return Objects.hash(id, name); } }
有没有可能两个不相等的对象有相同的hashcode? 有可能,即产生hash冲突。当hash冲突产生时,一般有以下几种方式来处理:
拉链法:每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表进行存储.
开放定址法:一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入
再哈希:又叫双哈希法,有多个不同的Hash函数.当发生冲突时,使用第二个,第三个….等哈希函数计算地址,直到无冲突.
java中有没有指针?java中是值传递引用传递? 有指针,但是隐藏了,开发人员无法直接操作指针,由jvm来操作指针java都是值传递 ,对于基本数据类型,传递是值的副本,而不是值本身。对于对象类型,传递是对象的引用,当在一个方法操作操作参数的时候,其实操作的是引用所指向的对象。
构造方法 constructor
构造方法是Java类中的一种特殊方法(可省略),用于在创建对象时进行初始化操作。 构造方法的名称必须与类名相同,并且没有返回类型,包括 void。构造方法通常用于设置对象的初始状态,为对象的属性赋初值或执行其他初始化任务。
方法也可以与class同名,区别在于方法必须要有void或具体返回值类型;
一个类可以拥有多个构造方法,只要它们的参数列表不同。这称为构造方法的重载。 1 2 3 4 5 6 7 public class MyClass { private int value; public MyClass () {this .value = 0 ;} public MyClass (int value) {this .value = value;} }
构造方法能不能显式调用? 不能,构造方法当成普通方法调用,只有在创建对象的时候它才会被系统调用
构造方法能不能重写,重载? 可以重载,但不能重写。
Java中有各种不同的类和代码块类型,让我们逐个解释它们:
普通类:是最基本的类类型,用于创建对象。它可以包含字段、方法、构造方法等。 1 2 3 public class MyClass { }
构造方法:是包含在类中的一组语句块,没有使用任何关键字。它在对象每一次创建时执行,可以用于初始化对象。 构造代码块是定义在类中的,不带任何修饰符(例如public、private等)。它在每次创建对象时都会执行,执行的时机在构造器调用之前。与实例初始化块不同,构造代码块不能被单独调用。 1 2 3 4 5 6 7 8 9 public class MyClass { MyClass() { } { } }
内部类:内部类是定义在另一个类内部的类。它有访问外部类成员的权限,并且可以用于实现一些封装和逻辑组织。 1 2 3 4 5 public class OuterClass { class InnerClass { } }
外部类: 外部类是普通的顶级类,不嵌套在其他类中。 1 2 3 public class OuterClass { }
静态代码块: 使用 static
关键字,包含在类中,用于在类加载时执行初始化操作。它仅执行一次。 1 2 3 4 5 6 public class MyClass { static { } }
静态内部类:是定义在另一个类内部的类,使用 static
修饰。与非静态内部类不同,它不依赖于外部类的实例。 1 2 3 4 5 public class OuterClass { static class StaticInnerClass { } }
静态 static
静态变量(Static Variables):被声明为 static
的成员变量,属于类而不是类的实例。它被所有类的实例共享,只有一个副本存在于内存中。 1 2 3 4 5 public class Counter { private static int count = 0 ; public static void increment () { count++; } }
静态方法(Static Methods):被声明为 static
的方法,它不需要实例化类就可以直接通过类名调用。静态方法不能访问非静态成员,也无法使用 this
关键字。 静态方法凭什么不能访问成员方法:因为成员方法属于对象实例,静态方法属于类本身,静态方法第一次加载(方法区)的时候还没有对象(堆),也就无法调用成员方法
静态代码块(Static Blocks):是包含在类中的静态块,它在类加载时执行,并且只执行一次。通常用于初始化静态变量或执行一些静态的初始化操作。
静态内部类(Static Inner Classes):在类中使用 static
关键字修饰的内部类。静态内部类与外部类实例无关,可以直接通过外部类名访问。 在使用静态成员时需要注意,它们的生命周期与类的生命周期相同,当类加载时会被初始化。静态成员属于类而不是对象,在合适的场景下能提供便利和效率。然而,过度使用静态成员可能会导致耦合度高和难以测试等问题,因此需要根据实际情况慎重使用。
内部类与静态内部类的区别?
静态内部类相对与外部类是独立存在的,在静态内部类中无法直接访问外部类中变量、方法。如果要访问的话,必须要new一个外部类的对象,使用new出来的对象来访问。但是可以直接访问静态的变量、调用静态的方法;
普通内部类作为外部类一个成员而存在,在普通内部类中可以直接访问外部类属性,调用外部类的方法。
如果外部类要访问内部类的属性或者调用内部类的方法,必须要创建一个内部类的对象,使用该对象访问属性或者调用方法。
如果其他的类要访问普通内部类的属性或者调用普通内部类的方法,必须要在外部类中创建一个普通内部类的对象作为一个属性,外同类可以通过该属性调用普通内部类的方法或者访问普通内部类的属性。
如果其他的类要访问静态内部类的属性或者调用静态内部类的方法,直接创建一个静态内部类对象即可。
静态变量、静态代码块、普通代码块和构造方法的执行顺序? 执行顺序可以总结为:静态变量(按定义顺序初始化) -> 静态代码块(按定义顺序执行) -> 普通代码块(对象实例化时按照定义顺序执行) -> 构造方法。
静态变量(静态成员变量):在类加载时按照定义的顺序依次执行初始化,不论该变量在类中定义的位置如何,只会初始化一次。
静态代码块(Static Blocks):静态代码块在类加载时执行,优先于普通代码块和构造方法。静态代码块只会执行一次。
构造代码块(普通初始化块):构造代码块在对象实例化时执行,在构造方法之前执行。每次创建对象都会执行一次。
构造方法(Constructor):构造方法在对象创建时执行,用于初始化对象。构造代码块执行完毕后执行。
子类继承父类,且都包含静态方法、构造方法,那么静态变量、静态代码块、普通代码块和构造方法的执行顺序? 顺序:父类静态方法 -> 父类静态代码块 -> 子类静态方法 -> 子类静态代码块 -> 父类构造代码块 -> 父类构造方法 -> 子类构造代码块 -> 子类构造方法
final在java中的作用,有哪些用法?
被final修饰的类不可以被继承
被final修饰的方法不可以被重写
被final修饰的变量不可以被改变.如果修饰引用,那么表示引用不可变,引用指向的内容可变.
被final修饰的方法,JVM会尝试将其内联,以提高运行效率
被final修饰的常量,在编译阶段会存入常量池中. 除此之外,编译器对final域要遵守的两个重排序规则更好:在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序初次读一个包含fifinal域的对象的引用,与随后初次读这个fifinal域,这两个操作之间不能重排序
Java中的继承是单继承还是多继承? 类是不支持多继承的,只能有一个父类, 类可以实现多个接口,这样就可以达到类对多个接口的”多继承”效果。接口(interface)是一种特殊的抽象类,它可以被类实现(implements)而不是被继承(extends)。接口中定义了一组抽象方法和常量,实现该接口的类必须实现接口中定义的所有抽象方法,并且可以拥有自己的字段和方法。
super() 与 this() 表示什么?
super表示当前类的父类对象,This表示当前类的对象。super()
和 this()
都是特殊的方法调用语句,用于调用构造器,并且都只能在构造器中使用,不能在普通方法中使用。另外,调用构造器时不能形成循环调用,即不能在同一个构造器中同时调用 super()
和 this()
。
super()
:用于调用父类的构造器。在子类的构造器中使用 super() 可以显式调用父类的构造器,并且必须作为子类构造器的第一条语句出现。如果子类构造器没有显式调用 super()
,则会默认调用父类的无参构造器。 1 2 3 4 5 6 public class Child extends Parent { public Child () { super (); System.out.println("Child constructor" ); } }
this()
:用于调用当前类的其他构造器。在一个类的构造器中使用 this() 可以调用同一类中的其他构造器,并且必须作为构造器的第一条语句出现。 1 2 3 4 5 6 7 8 9 10 11 12 13 public class MyClass { private int value; public MyClass () { this (0 ); System.out.println("Default constructor" ); } public MyClass (int value) { this .value = value; System.out.println("Parameterized constructor" ); } }
抽象类(Abstract Class):
特点 :抽象类是一种不能被实例化的类,通常用于定义其他类的结构和行为。它可以包含抽象方法(只有方法签名,没有具体实现),以及普通的方法和字段。一个类只能继承一个抽象类。可以包含构造函数,可以有访问修饰符(public、private、protected)的方法。子类必须实现抽象类中的所有抽象方法,除非子类也是抽象类。
使用场景 :当需要创建一个类,并在其中定义一些方法的行为,但不希望该类被实例化时,可以使用抽象类。抽象类也适合用于在类层次结构中作为其他类的基类,提供通用方法和字段,而具体实现交给其子类。
抽象方法的方法体不需要使用 {}
,抽象方法是指没有具体实现的方法。只需在方法签名后面加上分号即可,不需要提供方法体。 1 2 3 4 5 6 7 8 public abstract class AbstractClass { public abstract void abstractMethod () ; public void concreteMethod () { System.out.println("This is a concrete method." ); } }
普通类与抽象类有什么区别? 普通类不能包含抽象方法,抽象类可以包含抽象方法; 抽象类不能直接实例化,普通类可以直接实例化
抽象的方法是否可同时是静态的,是否可同时是本地方法(native),是否可同时被 synchronized修饰? 都不能。抽象方法需要子类重写,而静态的方法是无法被重写的,因此二者是矛盾的。本地方法是由本地代码(如 C 代码)实现的方法,而抽象方法是没有实现的,也是矛盾的。synchronized 和方法的实现细节有关,抽象方法不涉及实现细节,因此也是相互矛盾的。
abstract 和 final 同时用来修饰同一个类或方法? 如果一个类使用了abstract
修饰符,表示这个类是抽象类,不能被实例化,可以包含抽象方法。 如果一个类使用了final
修饰符,表示这个类是最终类,不能被其他类继承。 如果一个方法使用了public abstract
修饰符,表示这个方法是抽象方法,只有声明没有实现,需要在子类中实现。 所以,abstract final
不能同时修饰类或方法。public abstract final
也是不符合Java语法的组合。需要根据具体的需求和设计来选择适当的修饰符。
是否可以从一个静态方法内部发出对非静态方法的调用? 不可以,静态方法只能访问静态成员,因为非静态方法的调用要先创建对象,在调用静态方法时可能对象并没有被初始化。
接口(Interface):
特点:接口是一种完全抽象的类别,其中只包含方法的签名,但没有方法的实际实现。类可以实现多个接口,但接口不能包含字段或非抽象方法(在Java 8之后,引入了默认方法和静态方法)。接口就是某个事物对外提供的一些功能的声明,是一种特殊的java类,接口弥补了java单继承的缺点。
使用场景:当不同类需要共享某些行为,但它们属于不同的类层次结构时,接口是一个很好的选择。接口允许类定义一组规范,以确保实现类必须提供接口中定义的所有方法。可以使用接口来实现多态性,允许不同的类实现相同的接口并具有不同的行为。
接口有什么特点? 接口中声明全是public static final修饰的常量 接口中所有方法都是抽象方法 接口是没有构造方法的 接口也不能直接实例化 接口可以多继承
接口与抽象类!?
接口和抽象类都是为了实现代码的重用和提供一致的编程接口而设计的。然而,接口更多地用于定义规范和合同,以确保实现类提供特定的行为,而抽象类更多地用于提供一些通用的方法和行为实现。
抽象类和接口都用于实现多态性和提供一致的编程接口。它们通常用于大型项目中的类层次结构设计和代码组织。
在设计框架或库时,接口是一个有用的工具,因为它可以定义规范和标准,并允许用户通过实现接口来提供自定义行为。
抽象类用于将一些通用方法和字段提取到一个父类中,以便子类可以继承和共享这些功能。
抽象类和接口的区别?
抽象类:1. 抽象方法,只有行为的概念,没有具体的行为实现。使用abstract关键字修饰,没有方法体。子类必须重写这些抽象方法。2. 包含抽象方法的类,一定是抽象类。3. 抽象类只能被继承 ,一个类只能继承一个抽象类。
接口:1. 全部的方法都是抽象方法,属性都是常量 2. 不能实例化,可以定义变量。3. 接口变量可以引用具体实现类的实例 4. 接口只能被实现 ,一个具体类实现接口,必须实现全部的抽象方法 5. 接口之间可以多实现 6. 一个具体类可以实现多个接口,实现多继承现象
抽象类和接口有什么异同? 抽象类和接口都不能够实例化,但可以定义抽象类和接口类型的引用。一个类如果继承了某个抽象类或者实现了某个接口都需要对其中的抽象方法全部进行实现,否则该类仍然需要被声明为抽象类。接口比抽象类更加抽象,因为抽象类中可以定义构造器,可以有抽象方法和具体方法,而接口中不能定义构造器而且其中的方法全部都是抽象方法。抽象类中的成员可以是 private、默认、protected、public 的,而接口中的成员全都是 public 的。抽象类中可以定义成员变量,而接口中定义的成员变量实际上都是常量。有抽象方法的类必须被声明为抽象类,而抽象类未必要有抽象方法。
接口是否可继承接口?抽象类是否可实现接口?抽象类是否可继承具体类? 接口可以继承接口 ,而且支持 多重继承 。 抽象类 可以 实现接口 , 抽象类 可继承 具体类 也可以 继承抽象类 。
Anonymous Inner Class(匿名内部类)是否可以继承其它类?是否可以实现接口? 可以继承其他类或实现其他接口,在 Swing 编程和 Android 开发中常用此方式来实现事件监听和回调。
内部类可以引用它的包含类(外部类)的成员吗?有没有什么限制? 一个内部类对象可以访问创建它的外部类对象的成员,包括私有成员。
匿名类,匿名内部类。 Java中的两个相关但不同的概念,它们通常用于创建临时的、一次性的类实例。
匿名类:
概念: 匿名类是指没有明确命名的类,通常用于创建一个实现某个接口或继承某个类的对象。
语法: 匿名类的语法形式是通过 new
关键字创建一个对象的同时实现接口或继承类,并在花括号内定义类的实现。 1 2 3 SomeInterface obj = new SomeInterface () { };
使用场景: 匿名类通常用于创建简单的、一次性的类实例,不需要为其定义专门的类名。
匿名内部类:
概念: 匿名内部类是指定义在其他类内部、没有类名的类。通常使用它来实现接口或继承类,并在类的内部进行实现。
语法: 匿名内部类的语法形式与匿名类相似,但它通常在其他类的方法内部定义,而不是在类的成员变量或其他地方。 1 2 3 4 5 6 7 public class SomeClass { public void doSomething () { SomeInterface obj = new SomeInterface () { }; } }
使用场景: 匿名内部类通常用于在方法内部创建一个实现某个接口或继承某个类的临时对象,它有助于简化代码结构,避免为一次性的需求专门定义一个新的类。 总体而言,匿名类和匿名内部类都是用于创建临时的、一次性的类实例,通常在需要实现某个接口或继承某个类的情况下使用。在Java中,Lambda 表达式的引入也提供了一种更简洁的方式来实现函数接口的匿名类。
Java的四种引用,强弱软虚
强引用:是平常中使用最多的引用,强引用在程序内存不足(OOM)的时候也不会被回收 1 String str = new String ("str" );
软引用:在程序内存不足时,会被回收,使用方式: 1 2 3 SoftReference<String> wrf = new SoftReference <String>(new String ("str" ));
可用场景: 创建缓存的时候,创建的对象放进缓存中,当内存不足时,JVM就会回收早先创建的对象。
弱引用:只要JVM垃圾回收器发现了它,就会将之回收,使用方式: 1 WeakReference<String> wrf= newWeakReference<String> (str);
可用场景:Java源码中的j的java.util.WeakHashMap中的key就是使用弱引用,我的理解就是,一旦我不需要某个引用,JVM会自动帮我处理它,这样我就不需要做其它操作。
虚引用:虚引用的回收机制跟弱引用差不多,但是它被回收之前,会被放入ReferenceQueue中。注意哦,其它引用是被JVM回收后才被传入ReferenceQueue中的。由于这个机制,所以虚引用大多被用于引用销毁前的处理工作。还有就是,虚引用创建的时候,必须带有ReferenceQueue,使用例子: 1 PhantomReference<String> prf = newPhantomReference<String>(newString("str" ),newReferenceQueue<>());
可用场景:对象销毁前的一些操作,比如说资源释放等。Object.finalize() 虽然也可以做这类动作,但是这个方式即不安全又低效
???上诉所说的几类引用,都是指对象本身的引用,而不是指 Reference 的四个子类的引用
注解 Annotation 注解在我的理解下,就是代码中的特殊标记,这些标记可以在编译、类加载、运行时被读取,并执行相对应的处理。注解本身只是元数据,它本身不执行任何功能。具体的实现通常是在某个拦截器、切面(Aspect)或其他处理机制中完成的。 注解在开发中是非常常见的,比如Spring框架的 @Controller / @Param / @Select 等等。一些项目也用到lombok的注解,@Slf4j / @Data 等等。Java原生也有@Overried、@Deprecated、@FunctionalInterface等基本注解。 Java原生的基本注解大多数用于「标记」和「检查」还,此外有一种叫做元Annotation(元注解),所谓的元Annotation就是用来修饰注解的。 那你自己写过注解吗? @Passtoken,,测试时加上此注解,发送请求时不用验证登录信息,, JunboRestResponse:使用该注解,确保被注解的方法或类返回一个特定的“骏伯响应”格式。???如何实现??
4种标准元注解是哪四种? 元注解的作用是负责注解其他注解。Java5.0 定义了 4 个标准的 meta-annotation 类型,被用来提供对其它 annotation 类型作说明。
@Target 修饰的对象范围 @Target说明了Annotation所修饰的对象范围: Annotation可被用于 packages、types(类、接口、枚举、Annotation 类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch 参数)。在 Annotation 类型的声明中使用了 target可更加明晰其修饰的目标
@Retention 定义 被保留的时间长短 Retention 定义了该 Annotation 被保留的时间长短:表示需要在什么级别保存注解信息,用于描述注解的生命周期(即:被描述的注解在什么范围内有效),取值(RetentionPoicy) 由:1. SOURCE:在源文件中有效(即源文件保留)2. CLASS:在 class 文件中有效(即 class 保留)3. RUNTIME:在运行时有效(即运行时保留)4.
@Documented 描述-javadoc @Documented 用于描述其它类型的 annotation 应该被作为被标注的程序成员的公共 API,因此可以被例如 javadoc 此类的工具文档化。
@Inherited 阐述了某个被标注的类型是被继承的 @Inherited 元注解是一个标记注解,@Inherited 阐述了某个被标注的类型是被继承的。如果一个使用了@Inherited 修饰的 annotation 类型被用于一个 class,则这个 annotation 将被用于该class 的子类。
final、finalize()、finally
性质不同:1. final为关键字;2. finalize()为方法;3. finally为区块标志,用于try语句中;
作用:
final为用于标识常量的关键字,final标识的关键字存储在常量池中(在这里final常量的具体用法将在下面进行介绍);
finalize()方法在Object中进行了定义,用于在对象“消失”时,由JVM进行调用用于对对象进行垃圾回收,类似于C++中的析构函数;用户自定义时,用于释放对象占用的资源(比如进行I/0操作);
finally{}用于标识代码块,与try{}进行配合,不论try中的代码执行完或没有执行完(这里指有异常),该代码块之中的程序必定会进行;且finally{}中的 return也会比 try{}中的更早返回!!
Java中的异常体系是怎样的? Java中的所有异常都来自顶级父类Throwable。Throwable下有两个子类Exception和Error。
Error表示非常严重的错误,比如 java.lang.StackOverFlowError 和 Java.lang.OutofMemoryError,通常这些错误出现时,仅仅想靠程序自己是解决不了的,可能是虚拟机、磁盘、操作系统层面出现的问题了,所以通常也不建议在代码中去捕获这些Error,因为捕获的意义不大,因为程序可能已经根本运行不了了。
Exception表示异常,表示程序出现Exception时,是可以靠程序自己来解决的比如NullPointerException、legalAccessException等,我们可以捕获这些异常来做特殊处理。 Exception这种异常又分为两类:运行时异常 和 编译异常。
运行时异常(不受检异常):RuntimeException类及其子类表示JVM在运行期间可能出现的错误。比如说试图使用空值对象的引用(NullPointerException)、数组下标越界(ArrayIndexOutBoundException)。此类异常属于不可查异常,一般是由程序逻辑错误引起的,在程序中可以选择捕获处理,也可以不处理。
编译异常(受检异常):Exception中除RuntimeException极其子类之外的异常。如果程序中出现此类异常,比如说IOException、FileNotFoundException、SQLException,必须对该异常进行处理,否则编译不通过。在程序中,通常不会自定义该类异常,而是直接使用系统提供的异常类。
如何自定义一个异常 继承一个异常类,通常是RumtimeException或者Exception
异常的处理机制有几种? 异常捕捉:try…catch…finally,异常抛出:throws。
try catch finally,try里有return,finally还执行么? 执行,并且finally的执行早于try里面的return。结论: 1、不管有木有出现异常,finally块中代码都会执行; 2、当try和catch中有return时,finally仍然会执行; 3、finally是在return后面的表达式运算后执行的(此时并没有返回运算后的值,而是先把要返回的值保存起来,管finally中的代码怎么样,返回的值都不会改变,任然是之前保存的值),所以函数返回值是在finally执行前确定的; 4、finally中最好不要包含return,否则程序会提前退出,返回值不是try或catch中保存的返回值。
throw与thorws区别
位置不同
throws 用在函数上,后面跟的是异常类,可以跟多个;而 throw 用在函数内,后面跟的是异常对象。
功能不同:
throws 用来声明异常,让调用者只知道该功能可能出现的问题,可以给出预先的处理方式;throw 抛出具体的问题对象,执行到 throw,功能就已经结束了,跳转到调用者,并将具体的问题对象抛给调用者。也就是说 throw 语句独立存在时,下面不要定义其他语句,因为执行不到。
throws 表示出现异常的一种可能性,并不一定会发生这些异常;throw 则是抛出了异常,执行 throw 则一定抛出了某种异常对象。
两者都是消极处理异常的方式,只是抛出或者可能抛出异常,但是不会由函数去处理异常,真正的处理异常由函数的上层调用处理。
在Java的异常处理机制中,什么时候应该抛出异常,什么时候捕获异常?
异常相当于一种提示,如果我们抛出异常,就相当于告诉上层方法,我抛了一个异常,我处理不了这个异常,交给你来处理,而对于上层方法来说,它也需要决定自己能不能处理这个异常,是否也需要交给它的上层。
所以我们在写一个方法时,我们需要考虑的就是,本方法能否合理的处理该异常,如果处理不了就继续向上抛出异常,包括本方法中在调用另外一个方法时,发现出现了异常,如果这个异常应该由自己来处理,那就捕获该异常并进行处理。 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 public class ExampleService { public void lowerLevelOperation () throws SpecificException { throw new SpecificException ("Something went wrong at lower level" ); } public void higherLevelOperation () throws HigherLevelException { try { lowerLevelOperation(); } catch (SpecificException e) { System.out.println("Caught specific exception: " + e.getMessage()); throw new HigherLevelException ("Exception at higher level" , e); } } public static void main (String[] args) { ExampleService service = new ExampleService (); try { service.higherLevelOperation(); } catch (HigherLevelException e) { System.out.println("Caught higher level exception: " + e.getMessage()); } } }
Java 泛型、、
在Java中的泛型简单来说就是:在创建对象或调用方法的时候才明确下具体的类型 使用泛型的好处就是代码更加简洁(无需强制转换),程序更加健壮(编译期间没有警告,在运行期就无ClassCastException)
使用场景:操作集合的时候,List lists = new ArrayList<>(); 如果是其他场景的话,那就是在写「基础组件」的时候了:再明确一下泛型就是「在创建对象或调用方法的时候才明确下具体的类型」,而组件为了做到足够的通用性,是不知道「用户」传入什么类型参数进来的,所以在这种情况下用泛型就是很好的实践。
泛型是会擦除的,那为什么反射能获取到泛型的信息呢? 泛型的信息只存在编译阶段,在class字节码就看不到泛型的信息了。那为什么下面这段代码能获取得到泛型的信息呢? 可以理解为泛型擦除是有范围的,定义在类上的泛型信息是不会被擦除的。 Java 编译器仍在 class 文件以 Signature 属性的方式保留了泛型信息。Type作为顶级接口,Type下还有几种类型,比如TypeVariable、ParameterizedType、WildCardType、GenericArrayType、以及Class。通过这些接口我们就可以在运行时获取泛型相关的信息。
泛型实例:组件为了做到足够的通用性,是不知道「用户」传入什么类型参数进来的,所以在这种情况下用泛型就是很好的实践。要写组件,还是离不开Java反射机制(能够从运行时获取信息),所以一般组件是泛型 + 反射 来实现的。 1 2 3 4 5 6 7 8 9 public abstract class BaseDao <T> { public BaseDao () { Class clazz = this .getClass(); ParameterizedType pt = (ParameterizedType) clazz.getGenericSuperclass(); clazz = (Class) pt.getActualTypeArguments()[0 ]; System.out.println(clazz); } }
1 2 3 4 5 6 public class UserDao extends BaseDao <User> { public static void main (String[] args) { BaseDao<User> userDao = new UserDao (); } }
泛型中extends和super的区别 1、<?extends T> 表示包括T在内的任何T的子类 2、<?super T> 表求包括T在内的任何T的父类
Java对象创建过程?new一个对象的步骤?
???检查类符号引用:首先,JVM 会检查 new 关键字后面的类符号引用,确保在常量池中能找到对应的类。???2. 加载类:如果在常量池中找到了类符号引用,JVM 就会加载这个类,包括加载、连接(验证、准备、解析)和初始化阶段。
(1. )类加载检查:JVM 首先检查是否已经加载并验证了这个类的字节码。如果没有,JVM会通过类加载器(ClassLoader)加载这个类,并进行验证,确保其符合Java语言规范。这个过程可能包括解析类的依赖关系,解析字段和方法,以及进行类型检查。
分配内存:JVM 会根据类的定义,在堆内存中为对象分配内存空间。这个内存空间包括对象的所有成员变量。这个过程包括选择内存分配方式(如指针碰撞、空闲列表、TLAB)、分配内存并进行内存清零。
初始化对象:分配完内存空间后,JVM 会将对象的除对象头外的内存空间初始化为默认值(基本数据类型为 0,引用类型为 null)。最后,JVM 会设置对象的对象头,包括哈希码、GC 信息等元信息。
调用构造方法:执行对象的初始化逻辑,包括对成员变量进行赋值、执行一些初始化操作等。
返回对象引用:最后,new 操作符会返回一个指向新创建对象的引用,通过这个引用可以在程序中操作对象的属性和调用对象的方法。
这些步骤是创建一个对象的基本流程,无论是通过 new 关键字创建对象,还是通过反射、序列化等方式创建对象,都要经历这些步骤。
Java 反射 ? 简单来说,反射就是Java可以给我们在运行时获取类的信息 什么是「运行时」:在编译器写的代码是 .java 文件,经过javac 编译会变成 .class 文件,class 文件会被JVM装载运行(这里就是真正运行着我们所写的代码(虽然是被编译过的),也就所谓的运行时。 为什么要在「运行时」获取类的信息:其实就是为了让我们所写的代码更具有「通用性」和「灵活性」。一个好用的“工具”是需要兼容各种情况的,不知道用该“工具”的用户传入的是什么对象,但你需要帮他们得到需要的结果。例如 SpringMVC 你在方法上写上对象,传入的参数就会帮你封装到对象上;Mybatis可以让我们只写接口,不写实现类,就可以执行SQL;在类上加上@Component注解,Spring就帮你创建对象… 这些统统都有反射的身影:约定大于配置,配置大于硬编码。通过”约定”使用姿势,使用反射在运行时获取相应的信息(毕竟作为一个”工具“是真的不知道你是怎么用的),实现代码功能的「通用性」和「灵活性」
除了使用new创建对象之外,还可以使用 Java 反射可以创建对象,谁的效率高? 通过new创建对象的效率比较高。通过反射时,先找查找类资源,使用类加载器创建,过程比较繁琐,所以效率较低。
用 new 关键字创建对象到底是编译时的还是运行时的方式?有什么区别? new 创建对象是一种在编译时进行的方式。在编写代码时,通过 new 关键字可以直接在源代码中创建对象,在源代码被编译成字节码时就确定了对象的创建,然后在运行时,Java 虚拟机(JVM)会加载字节码文件,并根据 new 关键字创建对象。这时会分配内存、调用构造方法等,完成对象的初始化。 这种方式的主要特点是静态,因为对象的创建和初始化都是在编译时确定的。相比之下,使用反射等机制可以实现在运行时动态创建对象,但也更为灵活,因为它可以处理一些在编译时无法确定的类型和类。
java反射的作用 反射机制是在运行时,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意个对象,都能够调用它的任意一个方法。在java中,只要给定类的名字,就可以通过反射机制来获得类的所有信息。这种动态获取的信息以及动态调用对象的方法的功能称为Java语言的反射机制。
哪里会用到反射机制?
jdbc就是典型的反射,hibernate,struts等框架使用反射实现的。 1 Class.forName('com.mysql.jdbc.Driver.class' );
在 Spring 中使用反射机制:目的是为了实现框架的灵活性和可扩展性,使得开发人员能够通过配置和注解等方式,实现各种功能而无需修改源代码:
依赖注入:Spring 使用反射来实现依赖注入,即通过在配置文件或注解中声明依赖关系,Spring 在运行时动态地注入对象之间的依赖关系。通过反射,Spring 能够实例化和初始化对象,以及在运行时处理依赖注入。
Bean 的自动装配: Spring 的自动装配机制依赖于反射,它能够根据一定的规则自动将 Bean 与其他 Bean 进行关联。通过反射,Spring 可以动态地识别和连接相应的 Bean。
AOP(面向切面编程): 在 Spring 中,AOP 是通过动态代理和反射来实现的。通过反射,Spring 能够在运行时动态地创建代理对象,并在方法执行前后执行额外的逻辑。
Bean 的生命周期管理: Spring 容器可以通过反射来实现对 Bean 的生命周期的管理,包括实例化、初始化、销毁等。
动态代理:Spring 使用动态代理和反射来实现一些特定的功能,如事务管理。通过动态代理,Spring 能够在运行时创建代理对象,将横切逻辑织入到目标对象中。
处理注解:Spring 使用反射来处理注解,包括扫描类路径上的注解、解析注解的属性值等。通过反射,Spring 能够在运行时获取和处理注解信息。
反射机制的优缺点
优点:
能够运行时动态获取类的实例,提高灵活性;
与动态编译结合
缺点:
使用反射性能较低,需要解析字节码,将内存中的对象进行解析。解决方案:1、通过setAccessible(true)关闭JDK的安全检查来提升反射速度;2、多次创建一个类的实例时,有缓存会快很多; 3、ReflflectASM工具类,通过字节码生成的方式加快反射速度
相对不安全,破坏了封装性(因为通过反射可以获得私有方法和属性)
反射的实现方式:
获取想要操作的类的 Class 对象,他是反射的核心,通过 Class 对象我们可以任意调用类的方法。以下是常用的方法:
使用 Class.forName
静态方法(最安全/性能最好) 1 2 3 4 5 try { Class<?> myClass = Class.forName("com.example.MyClass" ); } catch (ClassNotFoundException e) { e.printStackTrace(); }
使用 ClassLoader
的 loadClass
方法: 也可以加载类并返回 Class
对象。 1 2 3 4 5 6 ClassLoader classLoader = getClass().getClassLoader();try { Class<?> myClass = classLoader.loadClass("com.example.MyClass" ); } catch (ClassNotFoundException e) { e.printStackTrace(); }
使用 .class
语法: 在已知类的情况下,可以直接获取该类的 Class
对象。 1 Class<?> myClass = MyClass.class;
使用对象的 getClass
方法: 可以返回该对象的 Class
对象。 1 2 MyClass myObject = new MyClass ();Class<?> myClass = myObject.getClass();
调用 Class 类中的方法,获取类的结构信息。 Java 反射 API:用来生成 ??? JVM 中的类、接口或则对象的信息。
Class 类:反射的核心类,可以获取类的属性,方法等信息。
Field 类:Java.lang.reflec 包中的类,表示类的成员变量,可以用来获取和设置类之中的属性值。
Method 类: Java.lang.reflec 包中的类,表示类的方法,它可以用来获取类中的方法信息或者执行方法。
Constructor 类: Java.lang.reflec 包中的类,表示类的构造方法。 1 2 3 4 5 6 7 8 9 Class clazz = Class.forName("reflection.Person" );Method[] method = clazz.getDeclaredMethods(); Field[] field = clazz.getDeclaredFields(); Constructor[] constructor = clazz.getDeclaredConstructors(); for (Constructor c : constructor) System.out.println(c.toString());
将获取到的类信息用于实际的操作,例如创建对象、调用方法、获取和设置字段的值等。 通过反射 API 来实现动态操作。
利用反射动态创建对象实例.
1 2 3 4 5 6 7 8 Class clazz = Class.forName("reflection.Person" );Person p = (Person) clazz.newInstance();Constructor c = clazz.getDeclaredConstructor(String.class,String.class,int .class); Person p1 = (Person) c.newInstance("李四" ,"男" ,20 );
深拷贝和浅拷贝 深拷贝和浅拷贝就是指对象的拷贝,一个对象中存在两种类型的属性,一种是基本数据类型,一种是实例对象的引用。 1,浅拷贝是指,只会拷贝基本教据类型的值,以及实例对象的引用地址,并不会复制一份引用地处所指的对象。也就是浅拷贝出来的对象,内部的类属性指向的是同一个对象 2,深拷贝是指,既会拷贝基本数据类型的值,也会针对实例对象的引用地址所指向的对象进行复制,深拷贝出来的对象,内部的属性指向的不是同一个对象
你了解动态代理吗???
代理模式是设计模式之一。代理模型有静态代理和动态代理。
静态代理需要自己写代理类,实现对应的接口,比较麻烦,在【编译期间】就确定好代理关系。
动态代理这一技术在实际或者框架原理中是非常常见的,在【运行期间】确定好代理关系。
动态代理有什么用:它是一种设计模式,用于在不修改原始对象的情况下,通过代理对象来间接访问原始对象,并在访问前后执行额外的操作。
动态代理中的对象:
目标对象:待增强的对象
代理对象:增强过后的对象,也就是我们使用的对象
两种实现方式:
JDK动态代理(反射)其实就是运用了反射的机制,会帮我们实现接口的方法,通过invokeHandler对所需要的方法进行增强;
目标对象 implement 目标接口
代理对象 implement 目标接口
目标对象和代理对象是平级关系
CGLIB代理(继承)则用的是利用ASM框架,通过修改其字节码生成子类来处理。
代理对象 extend 目标对象
目标对象和代理对象是父子关系
动态代理的应用:功能增强、控制访问。
SpringAOP
Spring整合Mybaits管理Mapper接口,不用写实现类,只写接口就可以执行SQL
面向对象设计中五大设计原则 SOLID、、
单一职责原则(SRP): 每个类或模块应该有且仅有一个引起它变化的原因,即一个类或模块应该只负责一种类型的任务或功能。这样可以提高代码的内聚性和可维护性,减少代码的复杂度。
开放封闭原则(OCP): 软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。即通过扩展现有的代码来实现新的功能,而不是修改已有的代码。这样可以降低修改已有代码带来的风险,并提高系统的稳定性。
里氏替换原则(LSP):子类必须能替换父类并出现在父类能够出现的任何地方,而不影响程序的正确性。即子类应该完全实现父类的方法,并且遵循父类的约定和契约。
接口隔离原则(ISP): 不应该强迫客户端依赖它们不需要的接口。接口应该小而专一,应该根据实际需要定义多个接口,而不是一个臃肿的接口。这样可以降低类之间的耦合度,提高系统的灵活性和可维护性。
依赖倒置原则(DIP):高层模块不应该依赖于低层模块,它们都应该依赖于抽象。抽象不应该依赖于具体实现细节,具体实现细节应该依赖于抽象。这样可以降低模块之间的耦合度,提高系统的灵活性和可扩展性。
常用的设计模式、、
策略模式 :定义了算法族,分别封装起来,让它们之间可以相互替换,此模式让算法的变化独立于使用算法的的客户。
使用场景:如果代码有多个 if…else 等条件分支,并且每个条件分支,可以封装起来替换的,我们就可以使用策略模式来优化。
模式实现:一个接口或者抽象类,里面两个方法(一个方法匹配类型,一个可替换的逻辑实现方法)、不同策略的差异化实现(即,不同策略的实现类)、使用策略模式
模式使用:我们借助 spring 的生命周期,使用 ApplicationcontextAware 接口,把对用的策略,初始化到 map 里面。然后对外提供 resolveFile 方法即可。
责任链模式 :实际上是一种处理请求的模式,它让多个处理器(对象节点)都有机会处理该请求,直到其中某个处理成功为止。责任链模式把多个处理器串成链,然后让请求在链上传递。
使用场景:当你想要让一个以上的对象有机会能够处理某个请求的时候,就使用责任链模式。责任链上,每个对象的差异化处理,如本小节的业务场景,就有参数校验对象、安全校验对象、黑名单校验对象、规则拦截对象
模式实现:一个接口或者抽象类、每个对象差异化处理、对象链(数组)初始化(连起来)
这个接口或者抽象类,需要:有一个指向责任下一个对象的属性、一个没置下一个对象的set方法、给子类对象有异化实现的方法(如以下代码的doFiter方法)
模版方法模式 :定义一个操作中的算法的骨架流程,而将一些步骤延迟到子类中,使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。它的核心思想就是:定义一个操作的一系列步骤,对于某些暂时确定不下来的步骤,就留给子类去实现,这样不同的子类就可以定义出不同的步骤。
模式实现:一个抽象类,定义骨架流程(抽象方法放一起)确定的共同方法步骤,放到抽象类(去除抽象方法标记)。不确定的步骤,给子类去差异化实现
观察者模式:行为模式,一个对象(被观察者)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知,进行广播通知。
它的主要成员就是观察者和被观察者。
被观察者(0bserverable):目标对象,状态发生变化时,将通知所有的观察者。
观察者(observer):接受被观察者的状态变化通知,执行预先定义的业务。
使用场景:完成某件事情后,异步通知场景。如,登陆成功,发个M消息等等。
模式实现:一个被观察者的类Observerable、多个观察者Observer、观察者的差异化实现、经典观察者模式封装:EventBus实战
工厂模式 :
工厂模式一般配合策略模式一起使用。用来去优化大量的if..else..或switc…cas…条件语句。
定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行。
单例模式:确保一个类只有一个实例,并提供一个全局访问点
工厂模式和抽象工厂的区别
简单工厂模式:
简单工厂模式是一种创建型设计模式,它通过一个工厂类来封装对象的创建过程,用户只需要通过工厂类来获取所需的对象,而无需直接调用对象的构造方法。
简单工厂模式只包含一个工厂类和多个产品类,工厂类根据用户的请求返回不同的产品对象。
简单工厂模式适用于对象类型较少、不需要频繁变化的情况下,对于新增产品类型或者修改产品构造方法时,需要修改工厂类的代码。
抽象工厂模式:
抽象工厂模式也是一种创建型设计模式,它通过一个抽象工厂接口和多个具体工厂类来创建一组相关或者相互依赖的对象,而无需指定具体的类。
抽象工厂模式包含抽象工厂接口、具体工厂类、抽象产品接口和具体产品类,每个具体工厂类负责创建一组相关的产品对象。
抽象工厂模式适用于需要创建一组相关或者相互依赖的产品对象,并且对产品的具体类型和实现进行解耦的情况下,当需要新增产品类型时,只需要添加新的具体工厂类和对应的产品类,无需修改现有代码。
综上所述,简单工厂模式主要用于创建单一类型的对象 ,工厂类负责根据用户的请求返回相应的产品对象;而抽象工厂模式主要用于创建一组相关或者相互依赖的产品对象 ,通过抽象工厂接口和具体工厂类来实现对象的创建,并且对产品的具体类型和实现进行解耦,适用于产品类型频繁变化的情况。
手写单例模式
懒汉式单例模式:只有在需要时才会创建实例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class Singleton { private static Singleton instance; private Singleton () { } public static Singleton getInstance () { if (instance == null ) { synchronized (Singleton.class) { if (instance == null ) { instance = new Singleton (); } } } return instance; } }
饿汉式单例模式:如果需要在类加载时就创建实例,可以直接在静态变量中初始化 1 2 3 4 5 6 7 8 9 public class Singleton { private static Singleton instance = new Singleton (); private Singleton () { } public static Singleton getInstance () { return instance; } }
对序列化和反序列化的理解?
序列化:把内存中的对象转换为字节流,以便实现存储和运输
反序列化:根据从网络或文件获取的对象的字节流,根据字节流中保存的对象描述信息和状态,重新构建一个新的对象
序列化的目的是为了解决网络通信中的对象传输的问题,把当前jvm进程中的对象跨网络传输到另一个jvm进程中并恢复;为保证通信双方对对象的可识别,会把对象先转换为通用的解析格式如:json,xml、、再转换为字节流进行运输
实现方法:序列化对象实现Serializable
接口,并再对象中添加serialVersionUID
字段。
什么时候用assert assertion(断言)在软件开发中是一种常用的调试方式,很多开发语言中都支持这种机制。在实现中,assertion就是在程序中的一条语句,它对一个boolean表达式进行检查,一个正确程序必须保证这个boolean表达式的值为true;如果该值为false,说明程序已经处于不正确的状态下,系统将给出警告或退出。一般来说,assertion用于保证程序最基本、关键的正确性。assertion检查通常在开发和测试时开启。为了提高性能,在软件发布后,assertion检查通常是关闭的
Java有没有goto java中的保留字,现在没有在java中使用
???拦截器与过滤器?
过滤器(Filter):
过滤器是 Servlet 规范中定义的一种组件,用于对请求进行预处理、后处理以及过滤。过滤器可以在请求进入 Servlet 之前进行预处理,也可以在响应返回客户端之前进行后处理。过滤器主要用于对请求和响应进行修改、验证、记录日志等操作。
过滤器可以通过在 web.xml
配置文件中进行配置,也可以通过注解 @WebFilter
来声明。过滤器需要实现 javax.servlet.Filter
接口,并实现其中的 init
、doFilter
和 destroy
方法。
过滤器可以对所有的请求进行过滤,例如对 URL 模式进行匹配,也可以通过编程方式动态地添加或移除过滤器。
拦截器(Interceptor):
拦截器是 Spring 框架提供的一种机制,用于对请求进行预处理和后处理。拦截器是基于面向切面编程(AOP)的思想,可以对控制器方法进行拦截,对请求进行前置处理、后置处理、异常处理等操作。
拦截器是 Spring MVC 框架的一部分,通过实现 HandlerInterceptor
接口来定义拦截器,并通过配置文件或者 Java 配置类进行声明和注册。
拦截器可以精确地对指定的控制器方法进行拦截,可以在请求处理之前或之后进行操作,并且可以对 Model 和 View 进行修改或者增强。
区别,主要体现在以下几个方面:
所处框架 : 过滤器(Filter)是 Servlet 规范中的一部分,用于对请求和响应进行预处理和后处理。它是在 Web 容器层面的一种功能。 拦截器(Interceptor)是 Spring MVC 框架中的一部分,用于对控制器方法进行拦截和处理。它是在 Spring MVC 框架的控制器层面的一种功能。
实现方式 : 过滤器需要实现 javax.servlet.Filter
接口,并实现其中的 init
、doFilter
和 destroy
方法。过滤器可以通过 web.xml
配置文件中进行配置,也可以通过注解 @WebFilter
来声明。 拦截器需要实现 HandlerInterceptor
接口,并实现其中的 preHandle
、postHandle
和 afterCompletion
方法。拦截器的声明和注册通常是通过配置文件或者 Java 配置类来完成。
功能特性 : 过滤器对请求和响应进行处理,可以进行内容修改、请求重定向、日志记录等操作。过滤器可以对所有的请求进行过滤。 拦截器主要用于对控制器方法进行拦截,可以在请求处理之前或之后进行操作,例如权限验证、日志记录、异常处理等。拦截器可以精确地对指定的控制器方法进行拦截。
使用场景 : 过滤器适用于对 Web 应用的全局请求进行处理,例如字符编码过滤、安全过滤、日志记录等。 拦截器适用于对控制器方法的请求进行处理,例如权限控制、日志记录、异常处理等。
日志、、 1.选择恰当的日志级别 error warn info debug 2.日志要打印出参入参数 方便甩锅 3.选择合适的日志格式 时间戳 线程名字 日志级别等 4.if-else ,switch 等分支语句都建议打印日志,方便排查 5.对一些比较低的日志级别进行判断,使用log.isXXXX()方法判断 6.不建议直接使用log4j ,logback等日志系统,建议使用slf4j框架,方便统一处理 7.建议使用参数占位符{},而不是+拼接,简洁且提升性能 8.建议使用异步日志,能有效提升IO性能 9.不要使用 e.printStackTrace() 打印错误信息,因为太多信息,且是堆栈信息,会使得内存溢出 10.异常不要只打一半,要完成输出 11.禁止在线上开启debug 会把磁盘打满 12.不要记录了异常,又抛出异常 13.避免重复打印日志,浪费磁盘空间 14.日志文件分离,不同级别日志存放在不同文件中 15.核心功能模块,建议打印详细的日志
函数式接口、、
函数式接口具有以下主要特点: 函数式接口只包含一个抽象方法,但可以包含多个默认方法或静态方法。 函数式接口可以使用 @FunctionalInterface 注解来显式声明,这样可以让编译器进行检查,确保其满足函数式接口的定义。 函数式接口可以通过 Lambda 表达式、方法引用等方式进行实例化。
函数式接口的引入使得 Java 可以更加方便地支持函数式编程风格,包括: 更简洁的代码:通过 Lambda 表达式可以编写更加简洁、可读性更强的代码。 支持并行操作:函数式接口可以很好地配合 Stream API 使用,支持并行操作和函数式变成。 提升代码灵活性:函数式接口的使用可以提升代码的灵活性和可维护性,使得代码更易于扩展和修改。
函数式接口在实际应用中有很多场景,例如: 在并发编程中,可以使用函数式接口配合 CompletableFuture 来进行异步任务处理。 在集合操作中,使用函数式接口可以简化集合的筛选、映射等操作。 在事件处理和回调机制中,使用函数式接口可以定义事件处理器。
集合有什么。 Java 集合 主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。
Collection包结构,与Collections的区别 Collection是集合类的上级接口,子接口有 Set、List、LinkedList、ArrayList、Vector、Stack、Set; Collections是集合类的一个帮助类, 它包含有各种有关集合操作的静态多态方法,用于实现对各种集合的搜索、排序、线程安全化等操作。此类不能实例化,就像一个工具类,服务于Java的Collection框架。
说说 List, Set, Map三者的区别 List(对付顺序的好帮手): List接口存储一组不唯一(可以有多个元素引用相同的对象),有序的对象 Set(注重独一无二的性质):不允许重复的集合。不会有多个元素引用相同的对象。 Map(用Key来搜索的专家): 使用键值对存储。Map会维护与Key有关联的值。两个Key可以引用相同的对象,但Key不能重复,Key可以是任何对象。
Java 的 List? List在Java里边是一个接口,常见的实现类有ArrayList和LinkedList,ArrayList底层数据结构是数组,LinkedList链表。
ArrayList 实现了动态扩容。当new ArrayList()时,默认会有一个大小为0,空的 Object[] 数组。 第一次add添加数据的时候,会给数组初始化一个默认值 10 的大小,并将元素添加到数组中。使用ArrayList在每一次add的时候,会先去计算数组空间;如果空间是够的,直接追加上去;如果不够,那就得扩容。 每一次扩容原来的 1.5倍 ,新容量的计算方式为(oldCapacity * 3)/2 + 1 即原容量的1.5倍再加1,创建一个新的数组[newData]。然后使用System.arraycopy方法(底层为native方法实现)将旧数组[elementData]中的元素复制到新数组中。这是一个高效的底层数组拷贝操作,避免了逐个元素复制的开销。最后,将新数组[newData]替换为ArrayList的内部数组[elementData]。 日常开发中用得最多的是ArrayList呢:是由底层的数据结构来决定的,在日常开发中,遍历 的需求比增删要多,即便是增删也是往往在List的尾部添加就OK了。像在尾部添加元素,ArrayList的时间复杂度也就O(1)。
LinkedList 基于链表实现的,对于增删操作来说,由于链表节点的指针调整相对比较简单,删除或添加一个节点的开销是 O(1) 的。但在进行遍历和随机访问时,由于链表的非连续存储,性能相对较差。因此,在实际场景中,ArrayList 在随机访问和遍历方面的性能通常比 LinkedList 更好,而在频繁的增删 操作时,LinkedList 可能更具优势. LinkedList 还实现了 DeQueue,可以对头尾元素操作,所以 LinkedList 也可以当作队列使用。
Array与ArrayList有什么不一样? Array与ArrayList都是用来存储数据的集合。ArrayList底层是使用数组实现的,但是ArrayList对数组进行了封装和功能扩展,拥有许多原生数组没有的一些功能。我们可以理解成ArrayList是Array的一个升级版。
ArrayList 的 遍历
;
使用 for 循环: 1 2 3 for (int j = 0 ; j < list.size(); j++) { System.out.println(list.get(j)); }
使用增强型 for 循环(for-each 循环): 1 2 3 for (Integer num : list) { System.out.println(num); }
使用迭代器(Iterator): 1 2 3 4 5 Iterator<Integer> iterator = list.iterator(); while (iterator.hasNext()) { Integer num = iterator.next(); System.out.println(num); }
ArrayList 的 remove
、、
根据索引删除元素;删除元素后,后面的元素会向前移动,列表的大小会减少。
根据对象删除元素:remove(Object o) 方法可以删除列表中第一次出现的指定对象。 1 list.remove(Integer.valueOf(5 ));
使用迭代器删除元素:使用 Iterator 遍历 ArrayList,并使用迭代器的 remove 方法安全地删除元素。 1 2 3 4 5 6 7 Iterator<Integer> iterator = list.iterator(); while (iterator.hasNext()) { Integer num = iterator.next(); if (num == 5 ) { iterator.remove(); } }
**ConcurrentModificationException
**异常:在遍历过程中直接调用 remove() 可能会引起并发修改异常。这是因为在遍历过程中修改了集合结构,导致迭代器的检测机制检测到并抛出异常。为避免,可采用以下方法:
在迭代过程中不修改集合的结构,可以通过复制集合或者使用迭代器的 remove 方法进行安全的删除操作。
在多线程环境下,可以使用线程安全的集合类(如 ConcurrentHashMap)或者采用同步机制(如使用 synchronized 关键字或者使用 Lock)来保证集合的线程安全性。
ArrayList如何实现线程安全?
使用Collections.synchronizedList
方法:这将返回一个线程安全的 List 包装器。通过这种方式,对 synchronizedList
的所有操作都会在内部被同步,从而确保线程安全。如果写入操作较为频繁,可能需要权衡使用 synchronizedList 或者其他并发集合类,具体根据业务需求来决定。 1 List<Type> synchronizedList = Collections.synchronizedList(new ArrayList <Type>());
使用CopyOnWriteArrayList
类:java.util.concurrent
包下的类,它通过在修改操作时复制整个数组来实现线程安全 。这意味着在写入操作时,它会创建一个新的数组,从而不影响正在进行的读取操作。如果读取操作频繁而写入操作较少,CopyOnWriteArrayList
可能是更好的选择,因为它对于并发读取操作而言性能较好。 1 List<Type> threadSafeList = new CopyOnWriteArrayList <Type>();
CopyOnWriteArrayList的底层原理是怎样的
首先CopyOnWriteArraylst内部也是用过数组来实现的,在向CpyOnWriteAraist添加元素时,会复制一个新的数组,写操作在新数组上进行,读操作在原数组上行
并且,写操作会加锁,防止出现并发写入丢失数据的问题
写操作结束之后会把原数组指向新数组
CopyOnWriteArraylist允许在写操作时来读取数据,大大提高了读的性能,因此适合读多写少的应用场,但会占内存,同时可能读到的数据不是实时最新的教据(写线程操作结束后才能读到新数据),所以不适合实时性要求很高的场景
Vector 你了解吗? Vector是底层结构是数组,一般现在已经很少用了。相对于ArrayList,它是线程安全的,在扩容的时候直接扩容两倍。 在Java中,Stack 类扩展了Vector,提供了一个后进先出(LIFO)的堆栈数据结构,其中元素的插入和删除都发生在堆栈的顶部。
set 集合 Set 注重独一无二的性质,该体系集合用于存储无序(存入和取出的顺序不一定相同)元素, 值不能重复。对象的相等性本质是对象 hashCode 值(java 是依据对象的内存地址计算出的此序号) 判断的, 如果想要让两个不同的对象视为相等的,就必须覆盖 Object 的 hashCode 方法和 equals 方法。
HashSet(Hash 表) 哈希表边存放的是哈希值。 HashSet 存储元素的顺序并不是按照存入时的顺序(和 List 显然不同) 而是按照哈希值来存的所以取数据也是按照哈希值取得。 元素的哈希值是通过元素的hashcode 方法来获取的, HashSet 首先判断两个元素的哈希值,如果哈希值一样,接着会比较equals 方法 如果 equls 结果为 true , HashSet 就视为同一个元素。如果 equals 为 false 就不是同一个元素。 哈希值相同 equals 为 false 的元素是怎么存储呢,就是在同样的哈希值下顺延(可以认为哈希值相同的元素放在一个哈希桶中)。
HashSet、HashMap 和 Hashtable 的关系。
HashSet
使用 HashMap
作为底层实现,用于存储不重复的元素。
HashMap
是键值对的存储结构,而 HashSet
只存储键。
Hashtable
也是键值对的存储结构,类似于 HashMap
,但是是同步的,因此适合于多线程环境。
HashMap
实现了 Map
接口。Hashtable
实现了 Map
接口以及 Dictionary
接口(在 Java 1.0 和 1.1 版本中使用较多,现已被 Map
接口取代)。
HashMap
和 Hashtable
允许键和值为 null
,而 HashSet
只允许一个 null
元素。 在实际开发中,一般推荐使用 HashMap
而不是 Hashtable
,因为 Hashtable
的同步性会带来额外的性能开销。如果需要在多线程环境下使用,也可以考虑使用 Collections.synchronizedMap()
方法来创建一个同步的 HashMap
。
TreeSet
TreeSet()是使用二叉树的原理对新 add()的对象按照指定的顺序排序(升序、降序),每增加一个对象都会进行排序,将对象插入的二叉树指定的位置。
Integer 和 String 对象都可以进行默认的 TreeSet 排序,而自定义类的对象是不可以的, 自己定义的类必须实现 Comparable 接口,并且覆写相应的 compareToTo()函数,才可以正常使用。
在覆写 compare()函数时,要返回相应的值才能使 TreeSet 按照一定的规则来排序
比较此对象与指定对象的顺序。如果该对象小于、等于或大于指定对象,则分别返回负整数、零或正整数
LinkHashSet( HashSet+LinkedHashMap) 对于 LinkedHashSet 而言,它继承与 HashSet、又基于 LinkedHashMap 来实现的。LinkedHashSet 底层使用 LinkedHashMap 来保存所有元素,它继承与 HashSet,其所有的方法操作上又与 HashSet 相同,因此 LinkedHashSet 的实现上非常简单,只提供了四个构造方法,并通过传递一个标识参数,调用父类的构造器,底层构造一个 LinkedHashMap 来实现,在相关操作上与父类 HashSet 的操作相同,直接调用父类 HashSet 的方法即可。
Map 了解多少? Map在Java里边是一个接口,常见的实现类有 HashMap、LinkedHashMap、TreeMap 和 ConcurrentHashMap 在Java里边,哈希表的结构是数组+链表的方式。HashMap底层数据结构是数组+链表/红黑树;LinkedHashMap是数组+链表/红黑树+双向链表;TreeMap是红黑树;而ConcurrentHashMap是数组+链表/红黑树
HashMap
hashmap通过put(key,value)存储,通过get(key)获取。当传入key时,hashmap会调用hashcode()方法计算出hash值,根据 hash 值将 value 保存在 bucket 里
实现原理:其实就是有个 Entry 数组,Entry 保存了 key 和 value。当你要塞入一个键值对的时候,会根据一个 hash 算法计算 key 的 hash 值,然后通过数组大小 n-1 & hash
值之后,得到一个数组的下标,然后往那个位置塞入这个 Entry。(hashmap的底层是哈希表,哈希表的实现是数组+链表+红黑树)
为了解决 hash 冲突的问题,采用了链表法
在 JDK 1.7 及之前链表的插入采用的是头插法,即在链表的头部插入新的 Entry。在 JDK 1.8 的时候,改成了尾插法,并且引入了红黑树。当链表的长度大于 8 且数组大小大于等于 64 的时候,就把链表转化成红黑树,当红黑树节点小于 6 的时候,又会退化成链表。
为什么 JDK 1.8 要对 HashMap 做红黑树这个改动?主要是避免 hash 冲突导致链表的长度过长,这样 get 的时候时间复杂度严格来说就不是 O(1) 了,因为可能需要遍历链表来查找命中的 Entry。
为什么定义链表长度为 8 且数组大小大于等于 64 才转红黑树?不要链表直接用红黑树不就得了吗?因为红黑树节点的大小是普通节点大小的两倍,所以为了节省内存空间不会直接只用红黑树,只有当节点到达一定数量才会转成红黑树,这里定义的是 8(泊松分布)
为什么节点少于 6 要从红黑树转成链表?也是为了平衡时间和空间,节点太少链表遍历也很快,节约内存。
HashMap 默认大小为16,负载因子的大小为 0.75。
HashMap的大小只能是2次幂的,假设你传一个10进去,实际上最终HashMap的大小是16(具体的实现在tableSizeFor可以看到)把元素放进HashMap的时候,需要算出这个元素所在的位置(hash)。在HashMap里用的是位运算来代替取模,更加高效。HashMap的大小只能是2次幂时,才能合理用位运算替代取模。
负载因子的大小决定着哈希表的扩容和哈希冲突。比如默认的HashMap大小为16,负载因子为0.75,这意味着数组最多只能放16*0.75=12个元素,每次put元素进去的时候,都会检查HashMap的大小有没有超过这个阈值,一旦超过12,则哈希表需要扩容。如果把负载因子调高了,哈希冲突的概率会增高,同样会耗时(查找速度变慢了)
put :首先对key做hash运算,计算出该key所在的index。如果没碰撞,直接放到数组中,如果碰撞了,需要判断目前数据结构是链表还是红黑树,根据不同的情况来进行插入。假设key是相同的,则替换到原来的值。最后判断哈希表如果满了,扩容。
get :还是对key做hash运算,计算出该key所在的index,然后判断是否有hash冲突。假设没有冲突直接返回,假设有冲突则判断当前数据结构是链表还是红黑树,分别从不同的数据结构中取出。 在HashMap中怎么判断一个元素是否相同?首先会比较hash值,随后会用==运算符和equals()来判断该元素是否相同。如果只有hash值相同,那说明该元素哈希冲突了,如果hash值和equals() || == 都相同,那说明该元素是同一个。
Jdk1.7 到 Jdk1.8 HashMap 发生了什么变化(底层)?
1.7中底层是数组+链表,1.8中底层是数组+链表+红黑树,加红黑树的目的是提高HashMap插入和查询整体效率
1.7中链表插入使用的是头插法,1.8使用的是尾插法,因为1.8中插入key和value时需要判断链表元素个数,所以需要遍历链表统计链表元素个数,所以正好就直接使用尾插法
1.7中哈希算法比较复杂,存在各种右移与异或运算,1.8中进行了简化,因为复杂的哈希算法的目的就是提高散列性,来提供HashMap的整体效率,而1.8中新增了红黑树,所以可以适当的简化哈希算法,节省CPU资源
hash 函数的优化:
1.8后,在put元素的时候传递的Key,先算出正常的哈希值,然后与高16位做异或运算,产生最终的哈希值。相当于把高位和低位的特征进行组合,结果得到的数组位置的散列度一定会更高,可以增加了随机性,减少了碰撞冲突的可能性。 1 2 3 4 static final int hash (Object key) { int h; return (key == null ) ? 0 : (h = key.hashCode()) ^ (h >>> 16 ); }
HashMap 扩容 resize() :默认是扩原来的2倍(因为HashMap的大小只能是2次幂),扩的是数组不是链表。
1.7版本:
先生成长度为原来2倍的新数组
遍历老数组中的每个位置上的链表上的每个元素
取每个元素的key,并基于新数组长度,计算出每个元素在新数组中的下标
头插法将元素添加到新数组中去
所有元素转移完了之后,将新数组赋值给HashMap对象的table属性
1.8版本:
先生成新数组,长度是老数组的2倍
遍历老数组中的每个位置上的链表或红黑树
如果是链表,则直接将链表中的每个元素里新计算下标,并添加到新数组中去 (将链表重新链接,按照低位区和高位区重新分配到新数组;)
如果是红黑树,则先遍历红黑树,先计算出红黑树中每个元素对应在新数组中的下标位置:统计每个下标位置的元素个数,如果该位置下的元素个数超过了8,则生成一个新的红黑树,并将根节点的添加到新数组的对应位置;如果该位置下的元素个数没有超过8,那么则生成一个链表,并将链表的头节点添加到新数组的对应位置、 (调用split方法将红黑树重新切分为低位区和高位区2个链表;判断低位区和高位区链表的长度,链表长度小于6,则会进行取消树化的处理,否则会将新生成的链表重新树化;)
所有元素转移完了之后,将新数组赋值给HashMap对象的table属性
Java8为什么将HashMap的插入方法改为了尾插法?
头插法,即新插入的元素会插入到链表的头部,会产生以下问题:
破坏了链表元素的插入顺序:由于头插法是将新插入的元素插入到链表的头部,这样就导致链表的顺序与元素插入的顺序相反,不利于一些需要按照插入顺序遍历的场景。
容易引起链表环形问题:由于头插法需要修改链表头 ,这会导致在并发环境下,触发resize()时多个线程同时修改新数组的桶节点的链表头,可能会引起链表环形问题,使得链表无法正确遍历或者出现死循环的情况。 ??
尾插法,即新插入的元素会插入到链表的尾部,这样可以解决很多问题并且有以下优点:。
提高查询效率:尾插法使得链表元素的插入顺序与元素插入的顺序一致,从而方便了元素的查找和遍历操作,提高了HashMap的查询效率。
避免链表环形问题:尾插法是将新插入的元素插入到链表的尾部,不需要修改链表头,因此可以避免在并发环境下多个线程修改链表头导致的链表环形问题。
1.8中插入key和value时需要判断链表元素个数,所以需要遍历链表统计链表元素个数,所以正好就直接使用尾插法
Java8及以后的版本插入操作的平均时间要比Java8之前的版本快,差距在1ms左右,这是由于Java8将HashMap的插入方法改为了尾插法,避免了链表环形问题的发生,同时优化了哈希算法和查询效率,从而提高了HashMap的性能。
LinkedHashMap? LinkedHashMap 底层结构是数组+链表+双向链表,实际上它继承了 HashMap,在 HashMap 的基础上维护了一个双向链表 LinkedHashMap 把 HashMap 的 Entry数组 加了两个指针:before 和 after。就是要把塞入的 Entry 之间进行关联,串成双向链表;有了这个双向链表,我们的插入可以是有序的,这里的有序不是指大小有序,而是插入有序。LinkedHashMap在遍历的时候实际用的是双向链表来遍历的,所以LinkedHashMap的大小不会影响到遍历的性能 并且内部还有个 accessOrder 成员,默认是 false, 代表链表是顺序是按插入顺序来排的,如果是 true 则会根据访问顺序来进行调整,就是咱们熟知的 LRU 那种,如果哪个节点访问了,就把它移到最后,代表最近访问的节点。
TreeMap? TreeMap的底层数据结构是红黑树,TreeMap的key不能为null(如果为null,那还怎么排序呢)。 TreeMap有序是通过实现 Comparable 接口或者自定义实现一个 comparator 传入构造函数(如果comparator为null,那么就使用自然顺序 ),这样塞入的节点就会根据你定义的规则进行排序。因此它除了作为 Map 外,还可以用作双端队列。
Hashtable、HashMap、TreeMap?
Hashtable
不允许使用 null 键或 null 值。
使用场景:1、当需要确保数据的线程安全,且在多线程环境中共享 Map 时,可以考虑使用 Hashtable。2、由于其性能相对较低,推荐在遗留代码中或者特定要求线程安全的小规模数据集合中使用。
HashMap
允许一个 null 键和多个 null 值。
使用场景: 1、在非多线程环境中,或者在读多写少的场景下(可以通过外部同步来解决线程安全问题),HashMap 是一个优选,因为它提供了更好的性能。 2、当需要快速查找、插入和删除键值对时,特别是在数据量较大的情况下。
当需要多线程操作的时候可以使用线程安全的ConcurrentHashMap。ConcurrentHashMap虽然也是线程安全的,但是它的效率比Hashtable要高好多倍。因为ConcurrentHashMap使用了分段锁,并不对整个数据进行锁定。
TreeMap
key不能为null
使用场景:1、当需要一个总是保持排序状态的 Map 时,TreeMap 是最合适的选择。它适用于需要频繁地进行有序遍历或范围搜索的场景。 2、 在需要根据键进行排序的应用中,如时间线索引、自然排序的目录结构等。3、 当数据结构的大小频繁变动,且需要保持有序状态时,TreeMap 通常比维护一个 ArrayList 之后再排序要高效。
ConcurrentHashMap?
ConcurrentHashMap是线程安全的Map实现类,它在juc包下的。线程安全的Map实现类除了ConcurrentHashMap还有一个叫做Hashtable。当然了,也可以使用Collections来包装出一个线程安全的Map,但无论是Hashtable还是Collections包装出来的都比较低效(因为是直接在外层套synchronize); HashMap不是线程安全的,多线程环境下有可能会有数据丢失和获取不了最新数据的问题
ConcurrentHashMap 本质上是一个 HashMap,因此功能和 HashMap 一样,但是ConcurrentHashMap 在 HashMap 的基础上,提供了并发安全的实现。并发安全的主要实现是通过对指定的 Node 节点加锁,来保证数据更新的安全性
底层数据结构 JDK1.7底层采用分段的数组+链表实现 JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树
加锁的方式 JDK1.7采用Segment分段锁,底层使用的是ReentrantLock JDK1.8采用CAS添加新节点,(如果已存在节点)采用synchronized锁定链表或红黑二叉树的首节点,相对Segment分段锁粒度更细,性能更好
红黑树
Red-Black Tree是一种自平衡的二叉搜索树,它通过一些附加的信息(颜色标记)保持树的平衡。这种平衡性质确保了在最坏情况下对于各种基本动态集合操作(插入、删除、查找)的性能都有较好的上界,保证了树的高度是对数级别的。
在Java中,TreeMap
和 TreeSet
类使用红黑树来实现有序 映射和有序 集合。
红黑树具有以下几个特征:
节点颜色: 每个节点都带有颜色,可以是红色或黑色。
根节点和叶子节点: 根节点和所有叶子节点(NIL节点)都是黑色的。
相邻节点颜色: 相邻的节点不能都是红色。也就是说,红色节点不能直接相连,黑色节点可以相连。
从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。 这些规则确保了红黑树的关键性质,即任意一条从根到叶子的路径都不会超过最短路径的两倍长。这保证了红黑树在动态插入和删除操作时能够保持相对平衡,从而避免了出现极端不平衡的情况。
红黑树插入和删除:都可能导致树失去平衡,因此需要通过旋转和重新着色等操作来维护平衡性质。
插入操作:
将节点插入: 将新节点插入到红黑树的合适位置,通常是按照二叉搜索树的插入规则。
新节点着色为红色: 插入的新节点着色为红色,以便更容易维护平衡性质。
重新着色和旋转: 根据父节点、祖父节点、叔叔节点等之间的颜色关系,可能需要进行以下操作:
情况1: 如果父节点是黑色,那么不需要做额外操作,树仍然保持平衡。
情况2: 如果父节点是红色而叔叔节点也是红色,可以通过颜色翻转来保持平衡。
情况3: 如果父节点是红色而叔叔节点是黑色,并且当前节点是父节点的右子节点,可以通过左旋转和右旋转来保持平衡。
情况4: 如果父节点是红色而叔叔节点是黑色,并且当前节点是父节点的左子节点,可以通过右旋转来保持平衡。
删除操作:
执行普通的二叉搜索树删除操作: 将要删除的节点从树中删除,并根据子节点的情况进行适当的替换。
重新着色和旋转: 删除操作可能破坏了红黑树的平衡性质,因此可能需要进行以下操作:
情况1: 如果删除的节点或替代节点是红色,只需将替代节点着色为黑色。
情况2: 如果删除的节点是黑色,而替代节点是红色,可以将替代节点着色为黑色。
情况3: 如果删除的节点和替代节点都是黑色,可能需要通过重新着色和旋转来保持平衡。
Java 集合使用泛型的好处。
可以确保类型安全,避免强制类型转换,提高代码的可读性和可维护性,能够在编译时错误检测。
“泛型” 意味着编写的代码可以被不同类型的对象所重用。 以集合来举例,使用泛型的好处是我们不必因为添加元素类型的不同而定义不同类型的集合,如整型集合类,浮点型集合类,字符串集合类,而这并不是最重要的,因为这也只需把底层存储设置Object即可,添加的数据全部都可向上转型为Object。?? 更重要的是我们可以通过规则按照自己的想法控制存储的数据类型。?
泛型类
泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。和泛型方法一样,泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。因为他们接受一个或多个参数,这些类被称为参数化的类或参数化的类型。
类型通配符 一般是 使用 ? 代替具体的类型参数。 例如 List<?> 在逻辑上是List, List 等所有 List<具体类型实参>的父类。 1 2 3 4 5 6 7 8 9 public class Box <T> { private T t; public void add (T t) { this .t = t; } public T get () { return t; } }
类型擦除
Java 中的泛型基本上都是在编译器这个层次来实现的,在生成的字节代码中是不包含泛型中的类型信息的。 使用泛型的时候加上的类型参数,如 List 和 List<?> 等类型在编译时会被擦除为原始类型 List。JVM 看到的只是 List,而由泛型附加的类型信息对 JVM 是不可见的。这个过程就称为类型擦除。
类型擦除的基本过程是找到用来替换类型参数的具体类,一般是 Object。如果指定了类型参数的上界的话,则使用这个上界。编译器会将代码中的类型参数都替换成具体的类。这种替换的行为是泛型在 Java 中实现的一部分,它确保了泛型代码在运行时不会受到泛型类型信息的影响,从而保持了 Java 的向后兼容性。 1 2 3 4 5 6 7 8 9 10 11 12 public class MyClass <T> {} public class MyClass {} public <T> void myMethod (T item) {} public void myMethod (Object item) {}
??单例Bean和单例模式?什么是 spring 的内部 bean? 单例模式表示JVM中某个类的对象只会存在唯一一个。而单例Bean并不表示JVM中只能存在唯一的某个类的Bean对象。 只有将 bean 用作另一个 bean 的属性时,才能将 bean 声明为内部 bean。为了定义 bean,Spring 的基于 XML 的配置元数据在 或 中提供了 元素的使用。内部 bean 总是匿名的,它们总是作为原型。
SpringBean生命周期原理。
实例化Bean:反射的方式创建对象 对于BeanFactory容器,当客户向容器请求一个尚未初始化的bean时,或初始化bean的时候需要注入另一个尚未初始化的依赖时,容器就会调用createBean进行实例化。 对于ApplicationContext容器,当容器启动结束后,通过获取BeanDefinition对象中的信息,实例化所有的bean。
设置对象属性(依赖注入):populateBean(),循环依赖的问题(三级缓存) 实例化后的对象被封装在BeanWrapper对象中,紧接着,Spring根据BeanDefinition中的信息 以及 通过BeanWrapper提供的设置属性的接口完成依赖注入。
处理Aware接口:接着,Spring会检测该对象是否实现了xxxAware接口,并将相关的xxxAware实例注入给Bean。invokeAwareMethod(完成BeanName,BeanFactory,BeanClassLoader对象的属性设置)
如果这个Bean已经实现了BeanNameAware接口,会调用它实现的setBeanName(String beanId)方法,此处传递的就是Spring配置文件中Bean的id值;
如果这个Bean已经实现了BeanFactoryAware接口,会调用它实现的setBeanFactory()方法,传递的是Spring工厂自身。
如果这个Bean已经实现了ApplicationContextAware接口,会调用setApplicationContext(ApplicationContext)方法,传入Spring上下文;
BeanPostProcessor:如果想对Bean自定义处理,那么可以让Bean实现了BeanPostProcessor接口,那将会调用postProcessBeforeInitialization(Object obj, String s)方法。 使用比较多的有(ApplicationContextPostProcessor,设置ApplicationContext,Environment,ResourceLoader,EmbeddValueResolver等对象)
InitializingBean 与 init-method:如果Bean在Spring配置文件中配置了 init-method 属性,则会自动调用其配置的初始化方法。 invokenitmethod(),判断是否实现了initializingBean接口,如果有,调用afterPropertiesset方法,没有就不调用
调用BeanPostProcessor的后置处理方法:若存在与 bean 关联的任何 BeanPostProcessors,则将调用 postProcessAfterInitialization()方法。 spring的aop就是在此处实现的,AbstractAutoProxyCreator。注册Destuction相关的回调接口:钩子函数
获取到完整的对象,可以通过getBean的方式来进行对象的获取
DisposableBean:当Bean不再需要时,会经过清理阶段,如果Bean实现了DisposableBean这个接口,会调用其实现的destroy()方法;
destroy-method:最后,如果这个Bean的Spring配置中配置了destroy-method属性,会自动调用其配置的销毁方法
Spring中Bean是线程安全的吗 Spring本身并没有针对Bean做线程安全的处理,所以: 1,如果Bean是无状志的,那么Bean则是线程安全的 2,如果Bean是有状态的,那么Bean则不是线程安全的 另外,Bean是不是线程安全,跟Bean的作用域没有关系,Bean的作用域只是表示Bean的生命周期范围,对于任何生命周期的Bean都是一个对象,这个对象是不是线挥安全的还得看这个Bean本身。
Spring 是如何解决循环依赖问题的? 如果两个或多个 Bean 互相之间持有对方的引用就会发生循环依赖。循环的依赖将会导致注入死循环。这是 Spring 发生循环依赖的原因。循环依赖有三种形态:https://www.bilibili.com/video/BV1Mc411S7B7?p=12&vd_source=ff210768dfaee27c0d74f9c8c50d7274 、、
三级缓存的作用是什么?Spring 中哪些情况下,不能解决循环依赖问题? p156 、、
缓存的放置时间、删除时间,,https://www.bilibili.com/video/BV1Mc411S7B7?p=14&vd_source=ff210768dfaee27c0d74f9c8c50d7274
Spring AOP AOP(Aspect-Oriented Programming), 即 面向切面编程, 它与OOP( Object-Oriented Programming, 面向对象编程) 相辅相成, 提供了与OOP 不同的抽象软件结构的视角. 在 OOP 中, 我们以类(class)作为我们的基本单元, 而 AOP 中的基本单元是 Aspect(切面) Spring AOP 解决的是「重复性」的非业务代码抽取的问题。所谓「面向切面编程」在我理解下其实就是在方法前后增加非业务代码,将那些与业务无关,却为业务模块所同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可扩展性和可维护性。 AOP 底层的技术是动态代理,???在Spring内实现依赖的是BeanPostProcessor。如果要代理的对象实现了某个接口,那么SpringAOP就会使用JDK动态代理去创建代理对象;而对于没有实现接口的对象,就无法使用IDK动态代理,转而使用CGlib动态代理生成一个被代理对象的子类来作为代理。
AOP 可以用于的一些常见场景:
权限控制: AOP 可以用于在方法执行前或执行后添加权限控制的逻辑,例如检查用户是否有足够的权限执行某个操作。这样可以避免将权限检查逻辑散布在业务代码的各个地方。
事务控制: AOP 可以用于管理事务,例如在方法执行前开启事务,在方法执行后根据执行结果决定是提交事务还是回滚事务。这样可以确保事务的一致性和可靠性。
日志记录: AOP 可以用于添加日志记录逻辑,例如在方法执行前记录方法的输入参数,方法执行后记录返回结果。这样可以方便地进行日志管理和分析。
性能监控: AOP 可以用于添加性能监控逻辑,例如在方法执行前记录开始时间,在方法执行后记录结束时间,计算方法的执行时间。方便进行性能分析和优化。
异常处理: AOP 可以用于添加异常处理逻辑,例如捕获方法执行过程中的异常并进行统一处理。这样可以减少业务代码中的异常处理逻辑,提高代码的清晰度。
缓存管理: AOP 可以用于添加缓存管理逻辑,例如在方法执行前检查是否有缓存命中,如果有直接返回缓存结果,否则执行方法并将结果缓存起来。
AspectJ?Spring AOP和AspectJ AOP有什么区别?
AOP是一种思想,AspectJ是一种实现。SpringAOP中已经集成了AspectJ,AspectJ应该算得上是Java生态系统中最完整的AOP框架了。
SpringAOP是属于运行时增强,而Aspect是编译时增强。 SpringAOP基于代理(Proxying),而AspectJ基于字节码操作(BytecodeManipulation)。 SpringAOP已经集成了AspectJ,Aspect应该算得上是Java生态系统中最完整的AOP框架了。Aspect相比于SpringAOP功能更加强大,但是SpringAOP相对来说更简单。 如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择AspectJ,它比SpringAOP快很多。
什么是 Aspect(切面)? Aspect 由 pointcount 和 advice 组成, 它既包含了横切逻辑的定义, 也包括了连接点的定义. Spring AOP 就是负责实施切面的框架, 它将切面所定义的横切逻辑编织到切面所指定的连接点中. 简单地认为, 使用 @Aspect 注解的类就是切面. AOP 的工作重心在于如何将增强编织目标对象的连接点上, 这里包含两个工作: 1、如何通过 pointcut 和 advice 定位到特定的 joinpoint 上 2、如何在advice 中编写切面代码.
在SpringAOP中,关注点和横切关注的区别是什么?什么是连接点呢?切入点是什么?什么是通知呢,有哪些类型呢?
关注点是应用中一个模块的行为,一个关注点可能会被定义成一个我们想实现的一个功能。
横切关注点是一个关注点,此关注点是整个应用都会使用的功能,并影响整个应用,比如日志,安全和数据传输。
连接点代表一个应用程序的某个位置,在这个位置我们可以插入一个AOP切面,它实际上是个程序执行SpringAOP的位置。
切入点(JoinPoint)是一个或一组连接点,通知将在这些位置执行。可以通过表达式或匹配的方 式指明切入点。
通知(Advice)是个在方法执行前或执行后要做的动作(特定 JoinPoint 处的 Aspect 所采取的动作),实际上是程序执行时要通过SpringAOP框架触发的代码段。Spring切面可以应用五种类型的通知:
Before:这类 Advice 在 joinpoint 方法之前执行,并使用@Before 注解标记进行配置。
After Returning:这类型 Advice 在连接点方法正常执行后执行,并使用@AfterReturning 注解标记进行配置。
After Throwing:这些类型的 Advice 仅在 joinpoint 方法通过抛出异常退出并使用 @AfterThrowing 标记配置时执行。
After (finally): 在连接点方法之后执行,无论方法退出是正常还是异常返回,并使用 @After 注解标记进行配置。
Around:在连接点之前和之后执行,并使用@Around 注解标记进行配置。
AOP 有哪些实现方式?
静态代理:使用 AOP 框架提供的命令进行编译,在编译阶段就可生成 AOP 代理类,因此也称编译时增强;
编译时编织(特殊编译器实现)
类加载时编织(特殊的类加载器实现)。
动态代理:在运行时在内存中“临时”生成 AOP 动态代理类,因此也被称为运行时增强。
Spring AOP and AspectJ AOP 有什么区别? Spring AOP 基于动态代理方式实现;AspectJ 基于静态代理方式实现。 ???SpringAOP 仅支持方法级别的 PointCut;提供了完全的 AOP 支持,它还支持属性级别的 PointCut。
什么是编织(Weaving)? 为了创建一个 advice 对象而链接一个 aspect 和其它应用类型或对象,称为编织(Weaving)。在 Spring AOP 中,编织在运行时执行。
如何理解 Spring 中的代理? ???将 Advice 应用于目标对象后创建的对象称为代理。在客户端对象的情况下,目标对象和代理对象是相同的。
??? SpringAOP的底层实现
总: aop概念,应用场景,动态代理
分: bean的创建过程中有一个步骤可以对bean进行扩展实现,aop本身就是IOC 的一个扩展功能,所以在BeanPostProcessor的后置处理方法中来进行实现 1、代理对象的创建过程(advice,切面,切点) 2、通过jdk或者cglib的方式来生成代理对象 3、在执行方法调用的时候,会调用到生成的字节码文件中,直接回找到DynamicAdvisoredlnterceptor类中的intercept方法,从此方法开始执行 4、根据之前定义好的通知来生成拦截器链 5、从拦截器链中依次获取每一个通知开始进行执行,在执行过程中,为了方便找到下一个通知是哪个,会有一个CglibMethodlnvocation的对象,找的时候是从-1的位置一次开始查找并且执行的。
在工作中实际用到过AOP优化代码吗??? 有的。当时我用AOP来对我们公司现有的监控客户端进行封装 一个系统离不开监控,监控基本的指标有QPS、RT、ERROR等等。对外暴露的监控客户端只能在代码里写对应的上报信息(灵活,但会与业务代码掺杂在一起),于是我利用注解+AOP的方式封装了一把,只要方法/类上带有我自定义的注解;方法被调用时,就会上报AQS、RT等信息 实现了非业务代码与业务代码分离的效果(:
Spring DAO Spring DAO 使得 JDBC,Hibernate 或 JDO 这样的数据访问技术更容易以一种统一的方式工作。这使得用户容易在持久性技术之间切换。它还允许您在编写代码时,无需考虑捕获每种技术不同的异常。
Spring事务管理:
Spring框架提供的一种事务管理机制,它简化了事务管理的操作,并提供了对不同事务管理器的统一接口。两种方式:
声明式事务:通过在方法上使用 @Transactional 注解来声明事务。这种方式更为常用,允许开发者将精力集中在业务逻辑上,而不需要关心事务的开始、提交或回滚。 1 2 3 4 @Transactional public void myTransactionalMethod () { }
编程式事务:通过编写代码手动管理事务的开始、提交或回滚。虽然不太常用,但在一些特殊情况下可能会用到。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Autowired private PlatformTransactionManager transactionManager;public void myProgrammaticMethod () { DefaultTransactionDefinition def = new DefaultTransactionDefinition (); TransactionStatus status = transactionManager.getTransaction(def); try { transactionManager.commit(status); } catch (Exception e) { transactionManager.rollback(status); throw e; } }
Spring事务是如何回滚的?
即:Spring事务管理是怎么实现的? 考虑数据库事务:建立连接,开启事务->进行sql操作->成功,commit/失败,rollback,,发现sql操作就是业务逻辑,前后工作完全类似AOP的around
总:spring的事务是由aop来实现的,首先要生成具体的代理对象,然后按照aop的整套流程来执行具体的操作逻辑,正常情况下要通过通知来完成核心功能,但是事务不是通过通知来实现的,而是通过一个TansactionInterceptor 来实现的,然后调用invoke()实现具体的逻辑
???分: 1、先做准备工作,解析各个方法上事务相关的属性,根据具体的属性来判断是否开始新事务 2、当需要开启的时候,获取数据库连接,关闭自动提交功能,开起事务 3、执行具体的sql逻辑操作 4、在操作过程中,如果执行失败了,那么会通过completeTransactionAterThrowing看来完成事务的回滚操作,回滚的具体逻辑是通过doRollBack方法来实现的,实现的时候也是要先获取连接对象,通过连接对象来回滚 5、如果执行过程中,没有任何意外情况的发生,那么通过commitTransactionAfterReturning来完成事务的提交操作,提交的具体逻辑是通过doCommit方法来实现的,实现的时候也是要获取连接,通过连接对象来提交 6、当事务执行完毕之后需要清除相关的事务信息cleanupTransactionlnfo 如果想要聊的更加细致的话,需要知道Transactionlnfo,Transactionstatus,
Spring中的事务是如何实现的 1,Spring事务底层是基于数据库事务和AOP机制的 2,首先对于使用了@Transactional注解的Bean,Spring会创建一个代理对象作为Bean 3,当调用代理对象的方法时,会先判断该方法上是否加了@Transactional注解 4,如果加了,那么则利用事务管理器创建一个数据库连接、 5,并且修改数据库连接的autocommit属性为false,禁止此连接的白动提交,这是实现Spring事务非常重要的一步 6,然后执行当前方法,方法中会执行sql 7。执行完当前方法后,如果没有出现异常就直接提交事务 8,如果出现了异常,并且这个异常是需要回滚的就会回滚事务,否则仍然提交事务 9,Spring事务的隔离圾别对应的就是数据库的隔离级别 10,Spring事务的传播机制是Spring事务自己实现的,也是Spring事务中最复杂的 11,Spring事务的传机制是基于数据库连接来做的,一个数据库连接一个事务,如果传机制配置为需要新开一个事务,那么实际上就是先建立一个数库连接,在此新数据库连接上执行sql
Spring事务传播机制 多个事务方法相互调用时,事务如何在这些法间传摇,方法A是一个事务的方法,方法A执行过程中调用了方法B,那么方法B有无事务以及方法B对事务的要求不同都会对方法A的事务具体执行造成影响,同时方法A的事务对方法B的事务执行也有影响,这种影响具体是什么就由两个方法所定义的事务传播类型所决定。 1,REQUIRED(Spring默认的事务传播类型): 如果当前没有事务,则自己新建一个事务,如果当前存在事务,则加入这个事务 2,SUPPORTS:当前存在事务,则加加入当前事务,如果当前没有事务,就以非事务方法执行 3。MANDATORY:当前存在事务,则加入当前事务,如果当前事务不存在,则抛出异常。 4,REQUIRES_NEW: 创建一个新事务,如果存在当前事务,则挂起该事务。 5,NOT_SUPPORTED: 以非事务方式执行如里当前存在事务,则持起当前事务 6,NEVER:不使用事务,如果当前事务存在,则抛出异常 7,NESTED: 如果当前事存在,则在嵌套事务中执行,否则REQUIRED的操作一样 (开启一个事务)https://www.bilibili.com/video/BV1Mc411S7B7?p=18&vd_source=ff210768dfaee27c0d74f9c8c50d7274
Spring事务什么时候会失效? spring事务的原理是AOP,进行了切面增强,那么失效的根本原因是这个AOP不起作用了! 常见情况有如下几种 ??1、发生自调用,类里面使用this调用本类的方法(this通常省略),此时这个this对象不是代理类,而是UserService对象本身!解决方法很简单,让那个this变成UserService的代理类即可! 2、方法不是public的:@Transactional 只能用于 public 的方法上,否则事务不会失效,如果要用在非 public 方法上,可以开启 Aspectj 代理楼式 3、数据库不支持事务 4、没有被spring管理 5、异常被吃掉,事务不会回滚(或者抛出的异常没有被定义,默认为RuntimeException)
Spring中常见的设计模式。
单例模式 :Spring默认情况下,容器中的Bean是单例的,即每个容器中的Bean只有一个实例。有助提高性能和减少资源消耗
原型模式:指定作用域为prototype
工厂模式 :Spring通过工厂模式实现了IOC(Inversion of Control)容器,使得对象的创建和管理被委托给了Spring容器。 BeanFactory
代理模式 :Spring AOP就是通过代理模式实现的。AOP允许开发者在不修改原始代码的情况下,插入和控制横切关注点,如日志、事务等。
责任链模式:使用aop时先生成一个拦截器链
模板方法模式 : 在Spring中,JdbcTemplate 和 RedisTemplate 等模板类使用了模板方法模式,将通用的任务实现在模板方法中,而将具体实现留给子类。 postProcessBeanFactory,onRefresh,initPropertyValue
观察者模式:Spring的事件机制是基于观察者模式的。通过定义事件和监听器,应用程序可以订阅感兴趣的事件,从而实现松耦合的通信机制。
策略模式:Spring的资源加载、事务管理等功能用了策略模式,通过定义一组算法族分别封装起来,并使它们可以相互替换。
适配器模式:SpringAOP中通知(Advice)就是一种适配器模式的应用。通知包装了一个切面逻辑,使得它可以在切点(Join Point)上执行。
装饰者模式:BeanWrapper
适配器模式:Adapter
什么是 Spring Boot? 随着新功能的增加,spring 变得越来越复杂。如果必须启动一个新的 Spring 项目,我们必须添加构建路径或添加Maven 依赖关系,配置应用程序服务器,添加 spring 配置,即开始一个新的 spring 项目需要很多努力,因为我们现在必须从头开始做所有事情。 Spring Boot 最重要的就是起步依赖和自动配置: 使用starter
启动器能够自动依赖其他组件,减少了Maven配置的繁琐性;Spring Boot根据当前类路径和jar包自动配置bean,例如,添加spring-boot-starter-web
启动器即可拥有web功能,无需额外的配置。因此,Spring Boot 可以帮助我们以最少的工作量,更加健壮地使用现有的 Spring功能。
为什么要用SpringBoot?
独立运行和简化部署: Spring Boot内嵌了各种servlet容器(如Tomcat、Jetty、Undertow),使得应用能够独立运行,无需打成war包部署到外部容器。通过打包成可执行的jar包,所有依赖包都在一个jar包内,简化了部署和运维。
简化配置和自动配置: 使用starter
启动器能够自动依赖其他组件,减少了Maven配置的繁琐性。Spring Boot根据当前类路径和jar包自动配置bean,例如,添加spring-boot-starter-web
启动器即可拥有web功能,无需额外的配置。
无代码生成和XML配置: Spring Boot的配置过程中无需代码生成,也不需要XML配置文件,通过条件注解实现自动配置,进一步简化了项目配置。
应用监控和健康检测: Spring Boot提供了一系列端点用于监控服务和应用,实现健康检测等功能。
减少开发、测试时间和努力: 提供快速启动开发的默认值,减少了开发和测试的时间和工作量。
使用JavaConfig避免XML配置: 倡导使用JavaConfig替代XML配置,提高了代码的可读性和维护性。
提供意见发展方法: Spring Boot提供了一套推荐的开发方法,使得团队更容易达成共识,提高了代码的一致性和规范性。
基于环境的配置: 支持基于环境的配置,通过传递环境参数(例如-Dspring.profiles.active={environment}
),可以在不同环境中灵活配置应用程序。
如何创建 Spring Boot Projects ?
Spring Initiatlizr 让创建 Spring Boot 项目变的很容易,通过 start.spring.io 创建。
手动设置一个 Maven 项目为 Spring Boot 项目:
添加 Spring Boot 依赖: 在 Maven 项目的 pom.xml
文件中,添加 Spring Boot 的依赖。通常,你可以添加 spring-boot-starter
或其他特定的 Starter 依赖,具体依赖根据项目需求而定。 1 2 3 4 5 6 7 8 <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter</artifactId > </dependency > </dependencies >
添加 Spring Boot 插件: 在 pom.xml
中添加 Spring Boot Maven 插件。插件用于打包和运行 Spring Boot 应用程序。 1 2 3 4 5 6 7 8 <build > <plugins > <plugin > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-maven-plugin</artifactId > </plugin > </plugins > </build >
创建 Spring Boot 主类: 在项目中创建一个包含 main
方法的主类,该类用于启动 Spring Boot 应用程序。这个类通常被注解为 @SpringBootApplication
。
添加其他 Spring Boot 配置: 根据项目需求,配置其他 Spring Boot 特性,如数据源、JPA、Web 支持等。可以通过配置文件(application.properties
或 application.yml
)或 Java 配置类进行配置。
Spring Boot 的核心注解是哪个?它主要由哪几个注解组成的? 启动类上面的注解是 @SpringBootApplication,它也是 Spring Boot 的核心注解,主要组合包含了以下3个注解: 1、@SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。 2、@EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项,如关闭数据源自动配置功能:@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })。 3、@ComponentScan:Spring组件扫描
Spring Boot中常用注解及其底层实现
@Bean注解:用来定义Bean,类似于XML中的bean>标签,Spring在启动时,会对加了@Bean注解的方法进行折,将方法的名字做为beanName,并通过执行方法得到bean对象
Bean处理
@Autowired:依赖注入
@Component:泛指组件
@Controller、@Service、@Repository
@RestController
@Configuration
Http请求:@GetMapping、@PostMapping、@PutMapping、@DeleteMapping
前后端参数传递:
@RequestParam:用在方法的参数前面,获取请求中表单类型的key=value格式的数据
@PathVariable:路径变量,参数与大括号里的名字一样要相同
@RequestBody:获取请求body中的数据,常用于搭配@PostMapping请求来提交对象数据
@ResponseBody:表示该方法的返回结果直接写入HTTP response body中,格式为json
读取配置
@value:直接读取各种配置源的属性名
@ConfigurationProperties:读取配置信息并与 bean 绑定
@PropertySource:指定加载自定义的配置文件
参数校验
Bean字段验证注解:@Null、@Min()、,,??
??@Valid:用于标注验证对象的级联属性
??@Validated:Spring提供的注解,于SpringMVC一起使用标注方法参数需要检查
统一异常处理
@ControllerAdvice:注解定义全局异常处理类,包含@Component所以可以被Spring扫描到
@ExceptionHandler:注解声明异常处理方法,表示遇到这个异常,就执行标注的方法
JPA数据持久化
@Entity:声明数据库实体类
@Table:设置表明
@ld:声明一个字段为主键
@GeneratedValue:声明主键的生成策略
@Column:声明字段,经常用于属性名和表字段的映射
@Transient:指定不需要持久化的字段
@Lob:声明某个字段为大字段
@Enumerated:声明枚举类型的字段
@Modifying:加在DAO方法上,提示是修改操作
@Transactional
作用于类上:表示所有该类的public 方法都配置相同的事务属性信息
作用于方法上:当类配置了@Transactional,方法也配置了@Transactional,方法的事务会覆盖类的事务配置信息
测试处理
@ActiveProfiles:常作思于测试类上,用于声明生效的 Spring 配置文件
@Test:声明一个方法为测试方法
@Transactional:被声明的测试方法的数据会回滚,避免污染测试数据
@WithMockUser:Spring Security 提供的,用来模拟一个真实用户,并且可以赋予权限
配置启动
@SpringBootApplication注解: 这个注解标识了一个SpringBoot工程,它实际上是另外三个注解的组合: a,@SpringBootConfiguration: 这个注解实际就是一个@Configuration,表示启动类也是一个配置类 b,@EnableAutoConfiguration:向Spring容器中导入了一Seletor,用来加ClassPath下SpringFactoties中所定义的自动配置类,将这些自动为配置Bean c,@ComponentScan:标识扫描路径,因为赋认是没有配置实际扫描路径,所以SpringBoot扫描的路径是启动类所在的当前目录
@Conditional
@ConditionalOnBean:配置了某个特定的Bean
@ConditionalOnMissingBean:没有配置特定的Bean
@ConditionalOnClass:Classpath里有指定的类
@ConditionalOnMissingClass:Classpath里没有指定的类
@ConditionalOnExpression:给定的SpEL表达式计算结果为true
@ConditionalOnJava:Java的版本匹配特定值或者一个范围值
@ConditionalonIndi:参数中给定的INDI位置必须存在一个,如果没有给参数,则要有JNDIInitialContext
@ConditionalOnProperty:指定的配置属性要有一个明确的值
@ConditionalOnResource:Classpath里没有指定的资源
@ConditionalOnWebApplication:这是一个Web应用程序
@ConditionalOnNotWebApplication:这不是一个Web应用程序
??说说SpringBoot自动配置 。 1、SpringBoot有着“约定大于配置”的理念,这一理念一定程度上可以用“SpringBoot自动配置”来解释。 通过 Spring Boot,我们可以快速开发基于 Spring 生态下的应用程序。 2、Spring Boot 约定由于配置的体现有很多,比如: Spring Boot Starter 启动依赖,它能帮我们管理所有 jar 包版本; 如果当前应用依赖了 spring mvc 相关的 jar,那么 Spring Boot 会自动内置Tomcat 容器来运行 web 应用,我们不需要再去单独做应用部署; Spring Boot 的自动装配机制的实现中,通过扫描约定路径下的 spring.factories 文件来识别配置类,实现 Bean 的自动装配; 默认加载的配置文件 application.properties 等等 3、在使用SpringBoot的时候,肯定会依赖于autoconfigure这么一个包,autoconfigure这个包里会有一个spring.factories文件,该文件定义了100+个入口的配置类。比如我们经常使用的redis、kafka等等这样常见的中间件都预置了配置类。当我们在启动SpringBoot项目的时候,内部就会加载这个spring.factories文件,进而去加载“有需要”的配置类。那我们在使用相关组件的时候,就会非常的方便(因为配置类已经初始化了一大部分配置信息)。一般我们只要在application配置文件写上对应的配置,就能通过各种template类直接操作对应的组件啦。 4、不是所有的配置类都会加载的,假设我们没有引入redis-starter的包,那Redis的配置类就不会被加载。具体Spring在实现的时候就是使用@ConditionalXXX
进行判断的。比如Redis的配置类就会有@ConditionalOnClass({RedisOperations.class})的配置,说明当前环境下如果有RedisOperations.class这个字节码,才会去加载Redis的配置类。
??Spring Boot 中自动装配机制的原理。 自动装配,简单来说就是自动把第三方组件的 Bean 装载到 Spring IOC 器里面,不需要开发人员再去写 Bean 的装配配置。在 Spring Boot 应用里面,只需要在启动类加上 @SpringBootApplication 注解就可以实现自动装配。 @SpringBootApplication 是一个复合注解,真正实现自动装配的注解是 @EnableAutoConfiguration 。自动装配的实现主要依靠三个核心关键技术。 1、引入 Starter 启动依赖组件的时候,这个组件里面必须要包含@Configuration 配置类,在这个配置类里面通过@Bean 注解声明需要装配到 IOC 容器的 Bean 对象。 2、这个配置类是放在第三方的 jar 包里面,然后通过 SpringBoot 中的约定优于配置思想,把这个配置类的全路径放在 classpath:/META-INF/spring.factories 文件中。这样 SpringBoot 就可以知道第三方 jar 包里面的配置类的位置,这个步骤主要是用到了 Spring 里面的 SpringFactoriesLoader 来完成的。 3、SpringBoot 拿到所第三方 jar 包里面声明的配置类以后,再通过 Spring 提供的ImportSelector 接口,实现对这些配置类的动态加载。 在我看来,SpringBoot 是约定优于配置这一理念下的产物,所以在很多的地方,都会看到这类的思想。它的出现,让开发人员更加聚焦在了业务代码的编写上,而不需要去关心和业务无关的配置。其实,自动装配的思想,在 SpringFramework3.x 版本里面的@Enable 注解,就有了实现的雏形。@Enable 注解是模块驱动的意思,我们只需要增加某个@Enable 注解,就自动打开某个功能,而不需要针对这个功能去做 Bean 的配置,@Enable 底层也是帮我们去自动完成这个模块相关 Bean 的注入。以上,就是我对 Spring Boot 自动装配机制的理解。
Spring Boot 属性源 1.命令行参数 2.JVM系统属性 3.操作系统环境变量 4.打包在应用程序内的 application.properties 或者 application.yml 文件 5.通过 @PropertySource 标注的属性源 6.默认属性
你对SpringBoot starter的理解? Starters是什么:Starters可以理解为启动器,它包含了一系列可以集成到应用里面的依赖包,你可以一站式集成Spring及其他技术,而不需要到处找示例代码和依赖包。 比如 Mybatis 框架会需要引入各种的包才能使用,而starter就是做了一层封装,把相关要用到的jar都给包起来了,并且也写好了对应的版本。这我们使用的时候就不需要引入一堆jar包且管理版本类似的问题了。 Starters包含了许多项目中需要用到的依赖,它们能快速持续的运行,都是一系列得到支持的管理传递性依赖。
常用的starter。 spring-boot-starter-web 嵌入tomcat和web开发需要servlet与jsp支持 spring-boot-starter-data-jpa 数据库支持 spring-boot-starter-data-redis redis数据库支持 spring-boot-starter-data-solr solr支持 mybatis-spring-boot-starter 第三方的mybatis集成starter
Spring Boot是如何启动Tomcat的 1,首先,SpringBoot在启动时会先创建一个Spring容器 2,在创建 Spring容器过程中,会利用 @ConditionalOnClass 技术来判断当前 classpath中是否存在 Tomcat 依赖,如果存在则会生成一个启动 tomcat 的Bean 3,Spring 容器创建完之后,就会获取启动 Tomcat 的Bean,并创建 Tomcat 对象,并绑定端口等,然后启动 Tomcat ( Spring Boot内嵌tomcat,与springmvc启动tomcat过程相反??
???Spring Boot中的监视器是什么?如何在 Spring Boot 中禁用 Actuator 端点安全性? Spring boot actuator是spring启动框架中的重要功能之一。Spring boot监视器可帮助您访问生产环境中正在运行的应用程序的当前状态。有几个指标必须在生产环境中进行检查和监控。即使一些外部应用程序可能正在使用这些服务来向相关人员触发警报消息。监视器模块公开了一组可直接作为HTTP URL访问的REST端点来检查状态 默认情况下,所有敏感的 HTTP 端点都是安全的,只有具有 ACTUATATOR 角色的用户才能访问它们。安全性是使用标准的HttpServletRequest.isUserInRole 方法实施的。 我们可以使用来禁用安全性。只有在执行机构端点在防火墙后访问时,才建议禁用安全性。
如何使用Spring Boot实现异常处理? Spring提供了一种使用ControllerAdvice处理异常的非常有用的方法。 我们通过实现一个ControlerAdvice类,来处理控制器类抛出的所有异常
SpringBoot 实现热部署有哪几种方式? 主要有两种方式:Spring Loaded,Spring-boot-devtools
如何实现 Spring Boot 应用程序的安全性? 为了实现 Spring Boot 的安全性,我们使用 spring-boot-starter-security 依赖项,并且必须添加安全配置。它只需要很少的代码。配置类将必须扩展WebSecurityConfigurerAdapter 并覆盖其方法。
Spring Boot 支持哪些日志框架?推荐和默认的日志框架是哪个? Spring Boot 支持 Java Util Logging, Log4j2, Lockback 作为日志框架,如果你使用 Starters 启动器,Spring Boot 将使用 Logback 作为默认日志框架
Spring Boot 2.X 有什么新特性?与 1.X 有什么区别? 配置变更 JDK 版本升级 第三方类库升级 响应式 Spring 编程 支持HTTP/2 支持配置属性绑定 更多改进与加强…
SpringBoot 集成 Mybatis 的过程 添加mybatis的starter maven依赖 org.mybatis.spring.boot mybatis-spring-boot-starter 1.2.0 在mybatis的接口中 添加@Mapper注解 在application.yml配置数据源信息
如何对Spring Boot应用进行测试? 在为Spring应用程序运行集成测试时,我们必须有一个 ApplicationContext。 为了简化测试,Spring Boot为测试提供了一个特殊的注释 @SpringBootTest。此批注从其 classes 属性指示的配置类创建 ApplicationContext。 如果未设置classes属性,Spring Boot将搜索主配置类。搜索从包含测试的包开始,直到找到使用@SpringBootApplication或@SpringBootConfiguration注释的类。 ???请注意,如果我们使用 JUnit 4 ,我们必须用 @RunWith(SpringRunner.class) 装饰测试类。
Spring Boot 项目结构
Spring Boot 项目通常遵循一种约定大于配置的原则,并提供了一种建议的项目结构,使得开发者可以更轻松地组织和管理项目。以下是一个常见的 Spring Boot 项目结构示例: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 my-spring-boot-project |-- src | |-- main | |-- java | | |-- com | | |-- example | | |-- MySpringBootApplication.java | | |-- controller | | |-- HomeController.java | | |-- service | | |-- MyService.java | |-- resources | |-- application.properties | | |-- static | | |-- templates |-- target |-- pom.xml
src/main/java
: 存放 Java 源代码。
com.example.MySpringBootApplication
: Spring Boot 应用程序的主类,包含 main
方法,用于启动应用程序。
com.example.controller
: 存放控制器类,处理 HTTP 请求。
com.example.service
: 存放服务类,处理业务逻辑。
src/main/resources
: 存放资源文件。
application.properties
: Spring Boot 应用程序的配置文件,用于配置各种属性。
static
: 存放静态资源文件,如 CSS、JavaScript 等。
templates
: 存放模板文件,如 Thymeleaf 模板。
target
: Maven 构建目录,包含编译后的类文件和构建产物。
pom.xml
: Maven 项目的配置文件,定义项目的依赖和构建配置。
最后聊下你是怎么看这块源码的??? 思路:我先从启动类开始,会有个@SpringBootApplication,后面会定位到一个自动配置的注解@EnableAutoConfiguration,那最后就能看到注解内部会去META-INF/spring.factories加载配置类
什么是 Spring Cloud? Spring cloud 流应用程序启动器是基于 Spring Boot 的 Spring 集成应用程序,提供与外部系统的集成。Spring cloud Task,一个生命周期短暂的微服务框架,用于快速构建执行有限数据处理的应用程序。
使用 Spring Cloud 有什么优势?
高可用性: 分布式架构能够提高系统的可用性。通过将系统划分为多个服务并分布在不同的节点上,即使某个节点或服务出现故障,整个系统仍然能够继续运行。
扩展性: 分布式系统可以更容易地进行横向扩展。通过增加节点或服务,系统可以更好地处理增加的负载,提高性能和吞吐量。
容错性: 分布式系统能够通过冗余和备份机制来提高容错性。即使某个节点或服务出现故障,备份节点或服务可以接管工作,保证系统的正常运行。
灵活性: 分布式系统能够更好地支持异构性,即不同类型的硬件、操作系统和编程语言。这使得系统更加灵活,能够选择最适合特定任务的技术栈。
资源共享: 分布式系统可以充分利用多个节点的计算和存储资源,实现资源的共享和最优化利用。
降低单点故障风险: 分布式系统通过将系统划分为多个部分,降低了单点故障的风险。即使某个节点或服务出现问题,其他部分仍然可以继续运行。
地理分布: 分布式系统支持地理分布,使得服务可以部署在不同的地理位置,提高服务的可用性和响应速度。
提高性能: 分布式系统可以通过并行计算和分布式存储来提高性能。任务可以同时在多个节点上执行,加速处理过程。
使用 Spring Boot 开发分布式微服务时,我们面临以下问题 1、与分布式系统相关的复杂性-这种开销包括网络问题,延迟开销,带宽问题,安全问题。 2、服务发现-服务发现工具管理群集中的流程和服务如何查找和互相交谈。它涉及一个服务目录,在该目录中注册服务,然后能够查找并连接到该目录中的服务。 3、冗余-分布式系统中的冗余问题。 4、负载平衡 –负载平衡改善跨多个计算资源的工作负荷,诸如计算机,计算机集群,网络链路,中央处理单元,或磁盘驱动器的分布。 5、性能-问题 由于各种运营开销导致的性能问题。 6、部署复杂性-Devops 技能的要求。
Spring Cloud 中的五大组件。
Eureka(服务注册与发现): 服务注册与发现框架。它用于管理各个微服务的状态,以实现服务之间的通信和发现。
Ribbon(负载均衡): Ribbon 是一个基于 HTTP 和 TCP 的客户端负载均衡工具,在服务消费者的请求中自动实现负载均衡,从而分摊到多个服务提供者上。
Feign(声明式服务调用): Feign 是一个声明式的伪 HTTP 客户端,它使得编写 HTTP 客户端变得更加简单。通过 Feign,开发者只需使用注解方式就可以轻松地调用服务,而不用手动处理 HTTP 请求和响应。
Hystrix(熔断器): 一种延迟和故障容错库,用于隔离访问远程服务、第三方库或者服务的点,防止故障蔓延到整个系统,从而提高系统的弹性和可用性。
Zuul(API 网关): Zuul 是 Netflix 提供的一个基于 JVM 路由和服务端的负载均衡器,用于构建微服务架构中的 API 网关,对外统一提供服务访问入口,并提供了路由、负载均衡、熔断、安全等功能。
服务注册和发现是什么意思?Spring Cloud 如何实现? 当我们开始一个项目时,我们通常在属性文件中进行所有的配置。随着越来越多的服务开发和部署,添加和修改这些属性变得更加复杂。有些服务可能会下降,而某些位置可能会发生变化。手动更改属性可能会产生问题。 Eureka 服务注册和发现可以在这种情况下提供帮助。由于所有服务都在 Eureka 服务器上注册并通过调用 Eureka 服务器完成查找,因此无需处理服务地点的任何更改和处理。
负载平衡的意义什么? 在计算中,负载平衡可以改善跨计算机,计算机集群,网络链接,中央处理单元或磁盘驱动器等多种计算资源的工作负载分布。负载平衡旨在优化资源使用,最大化吞吐量,最小化响应时间并避免任何单一资源的过载。使用多个组件进行负载平衡而不是单个组件可能会通过冗余来提高可靠性和可用性。负载平衡通常涉及专用软件或硬件,例如多层交换机或域名系统服务器进程。
什么是 Hystrix??? 它如何实现容错?Hystrix 是一个延迟和容错库,旨在隔离远程系统,服务和第三方库的访问点,当出现故障是不可避免的故障时,停止级联故障并在复杂的分布式系统中实现弹性。通常对于使用微服务架构开发的系统,涉及到许多微服务。这些微服务彼此协作
什么是微服务 微服务架构是一种架构模式或者说是一种架构风格,它提倡将单一应用程序划分为一组小的服务,每个服务运行在其独立的自己的进程中,服务之间相互协调、互相配合,为用户提供最终价值。服务之间采用轻量级的通信机制互相沟通(通常是基于HTTP的RESTful API),每个服务都围绕着具体的业务进行构建,并且能够被独立的构建在生产环境、类生产环境等。 另外,应避免统一的、集中式的服务管理机制,对具体的一个服务而言,应根据业务上下文,选择合适的语言、工具对其进行构建,可以有一个非常轻量级的集中式管理来协调这些服务,可以使用不同的语言来编写服务,也可以使用不同的数据存储。
什么是服务熔断?服务降级? 服务降级熔断机制是应对雪崩效应的一种微服务链路保护机制。当某个微服务不可用或者响应时间太长时,会进行服务降级,进而熔断该节点微服务的调用,快速返回“错误”的响应信息。当检测到该节点微服务调用响应正常后恢复调用链路。在SpringCloud框架里熔断机制通过Hystrix实现,Hystrix会监控微服务间调用的状况,当失败的调用到一定阈值,缺省是5秒内调用20次,如果失败,就会启动熔断机制。 服务降级,一般是从整体负荷考虑。就是当某个服务熔断之后,服务器将不再被调用,此时客户端可以自己准备一个本地的fallback回调,返回一个缺省值。这样做,虽然水平下降,但好歹可用,比直接挂掉强。
说说 RPC 的实现原理 首先需要有处理网络连接通讯的模块,负责连接建立、管理和消息的传输。其次需要有编解码的模块,因为网络通讯都是传输的字节码,需要将我们使用的对象序列化和反序列化。剩下的就是客户端和服务器端的部分,服务器端暴露要开放的服务接口,客户调用服务接口的一个代理实现,这个代理实现负责收集数据、编码并传输给服务器然后等待结果返回。
REST 和RPC对比 1.RPC主要的缺陷是服务提供方和调用方式之间的依赖太强,需要对每一个微服务进行接口的定义,并通过持续继承发布,严格版本控制才不会出现冲突。 2.REST是轻量级的接口,服务的提供和调用不存在代码之间的耦合,只需要一个约定进行规范。
微服务的优点缺点?说下开发项目中遇到的坑? 优点:1.每个服务直接足够内聚,代码容易理解2.开发效率高,一个服务只做一件事,适合小团队开发3.松耦合,有功能意义的服务。4.可以用不同语言开发,面向接口编程。5.易于第三方集成6.微服务只是业务逻辑的代码,不会和HTML,CSS或其他界面结合.7.可以灵活搭配,连接公共库/连接独立库 缺点:1.分布式系统的责任性2.多服务运维难度加大。3.系统部署依赖,服务间通信成本,数据一致性,系统集成测试,性能监控。
Spring Cloud 和 Dubbo 有哪些区别? Spring cloud是一个微服务框架,提供了微服务领域中的很多功能组件,Dubbo以开始是一个RPC调用框架,核心是解决服务调用间的问题,Springcloud是一个大而全框架,Dubbo则更侧重于服务调用,所以Dubbo所提供的功能没有Springcloud全面,但Dubbo的服务调用性能Springcloud高,不过并不对立的,是可以结合起来一起使用的。
Eureka和zookeeper都可以提供服务注册与发现的功能,请说说两个的区别? Zookeeper保证了CP(C:一致性,P:分区容错性),Eureka保证了AP(A:高可用) 1.当向注册中心查询服务列表时,我们可以容忍注册中心返回的是几分钟以前的信息,但不能容忍直接down掉不可用。也就是说,服务注册功能对高可用性要求比较高,但zk会出现这样一种情况,当master节点因为网络故障与其他节点失去联系时,剩余节点会重新选leader。问题在于,选取leader时间过长,30 ~ 120s,且选取期间zk集群都不可用,这样就会导致选取期间注册服务瘫痪。在云部署的环境下,因网络问题使得zk集群失去master节点是较大概率会发生的事,虽然服务能够恢复,但是漫长的选取时间导致的注册长期不可用是不能容忍的。 2.Eureka保证了可用性,Eureka各个节点是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点仍然可以提供注册和查询服务。而Eureka的客户端向某个Eureka注册或发现时发生连接失败,则会自动切换到其他节点,只要有一台Eureka还在,就能保证注册服务可用,只是查到的信息可能不是最新的。除此之外,Eureka还有自我保护机制,如果在15分钟内超过85%的节点没有正常的心跳,那么Eureka就认为客户端与注册中心发生了网络故障,此时会出现以下几种情况: ①、Eureka不在从注册列表中移除因为长时间没有收到心跳而应该过期的服务。 ②、Eureka仍然能够接受新服务的注册和查询请求,但是不会被同步到其他节点上(即保证当前节点仍然可用) ③、当网络稳定时,当前实例新的注册信息会被同步到其他节点。因此,Eureka可以很好的应对因网络故障导致部分节点失去联系的情况,而不会像Zookeeper那样使整个微服务瘫痪
Ribbon和Feign的区别? 1.都是调用其他服务的,但方式不同。 2.启动类注解不同,Ribbon是@RibbonClient,feign的是@EnableFeignClients 3.服务指定的位置不同,Ribbon是在@RibbonClient注解上声明,Feign则是在定义抽象方法的接口中使用@FeignClient声明。 4.调用方式不同,Ribbon需要自己构建http请求,模拟http请求然后使用RestTeTemplate发送给其他服务,步骤相当繁琐。Feign需要将调用的方法定义成抽象方法即可。
Spring Cloud Gateway? Spring Cloud Gateway是Spring Cloud官方推出的第二代网关框架,取代Zuul网关。网关作为流量的,在微服务系统中有着非常作用,网关常见的功能有路由转发、权限校验、限流控制等作用。 使用了一个RouteLocatorBuilder的bean去创建路由,除了创建路由RouteLocatorBuilder可以让你添加各种predicates和filters,predicates断言的意思,顾名思义就是根据具体的请求的规则,由具体的route去处理,filters是各种过滤器,用来对请求做各种判断和修改。
什么是 Ribbon负载均衡? 1.Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端 负载均衡的工具。 2.Ribbon客户端组件提供一系列完善的配置项如连接超时,重试等。简单的说,就是在配置文件中列出Load Balancer(简称LB)后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随机连接等)去连接这些机器。我们也很容易使用Ribbon实现自定义的负载均衡算法。