String、StringBuilder、StringBuffer有什么区别与联系?
String是final类型的,不可变,但是通过反射可以暴力修改。
Java语言规范对String做了如下说明:
String a = new String(“a”); 创建了两个对象或者一个
Java中的常量优化会使当前已知的字面量"a" + “b” + “c” 优化为"abc",而不是在常量池创建三个对象。
而类似这种下面的情况,则不会这么做,此时的s3指向的是堆中的字符串对象。
==:判断两个变量或实例是否指向同一个内存空间,即判断两个对象(的引用)是否是同一个。基本数据比较的是值,引用数据比较的是内存地址
equals:判断两个对象的内容是否相等,equals是判断两个变量或实例所指向的内存空间的值是否相同。或者字符串的内容是否相同
String中重写了equals来让其去比较对象的值,倒不如说一般都建议重写equals,因为底层源码中,equals就是==
public boolean equals(Object obj) {
return (this == obj);
}
hashCode与equals和==的一些相关规定
hashCode通用规定
一个程序应用在执行期间,只要对象的equals方法的比较所用到的信息没有被修改,那么对同一对象的多次调用hashCode应该返回同一个值。
两个对象的equals返回true,则要求调用两个对象的hashCode必须返回同一个结果。
两个对象的equals返回false,则不要求两个对象一定不等(重写equals的情况)。
这也是为什么重写equals必须重写hashCode,如果不重写的话,会有一种情况(在一些场景下我们要求equals返回true,他们的实际值相等,可他们并不是同一个对象。hashCode默认是根据对象的内存地址经过hash算法得来的,每个对象在堆中的存储不一样,那么hashCode一定不相等,违反了前面的第二条)。
使用Class.forName(String className)方法,传入某个类的全限定类名,例如JDBC。只能在运行期确认该类是否存在。
调用某个类的.class属性来返回对应的class对象。代码安全,程序在编译阶段就可以检查需要访问的Class是否存在,程序性能好。
Class clazz = ClassName.class;
调用getClass()方法。
反射:对于任意一个类,都能够知道这个类的所有属性和方法(包括静态属性和静态方法);对于任意一个对象,都能够调用它的任意一个方法和属性。即动态获取对象信息和调用对象方法的功能。反射还可以新建类的实例(对象)。
反射的优点:
缺点:
反射的具体应用场景:
使用就是获取某个类的class对象,然后利用该对象下的各种getXxx方法即可获取。
byte | char | short | int | long | float | double | boolean | |
---|---|---|---|---|---|---|---|---|
表示范围 | -2^7 到 2^7-1 | / | -2^15 到 2^15-1 | -2^31 到 2^31-1 | -2^63 到 2^63-1 | / | / | / |
所占字节 | 1 | 2 | 2 | 4 | 8 | 4 | 8 | 1 |
其中对于int,32 64位的区别就是在对象头上: 32位系统上占用8bytes,64位系统上占用16bytes;
装箱:将基本类型用它们对应的引用类型来包装,例如int->Integer
拆箱:将包装类型转换为基本数据类型
自动装箱:进行创建的时候 Integer a = 127; 就将127这个基本类型自动装箱成Integer。
自动拆箱:在Integer类型与int进行比较时,会把Integer自动拆箱成int类型进行比较。
Integer a = 128; //将127这个基本类型自动装箱成Integer
Integer b = 128; //将127这个基本类型自动装箱成Integer
int c = 128;
System.out.println(a == b); //不在在-128到127这个范围,会创建新的对象,所以a != b
System.out.println(a.equals(b)); //比较对象的值,直接就true
System.out.println(a == c); //将a自动拆箱成int的128和c进行比较,返回true
意义:Java是一种完全面向对象的语言,但是对于CPU来说,处理一个完整的对象需要很多指令,并且又需要很多内存。于是Java有一种机制,使得基本类型在一般的编程中被当做非对象的简单类型处理,另一些场合又允许他们是个对象,例如方法传值需要传递一个Object类型,还有就是泛型指定的时候。
相同点:
不同点:
抽象类 | 接口 | |
---|---|---|
声明 | abstract | interface |
实现 | extends,如果子类不是抽象类的话,子类需要提供抽象类所有声明的抽象方法的实现 | implements,实现类需要提供所有接口中方法的实现 |
构造器 | 抽象类可以有构造器,但不能创建实例 | 不可以有构造器 |
访问修饰符 | 方法可以是任意的访问修饰符,字段也是 | 方法为隐式的public abstract;变量为public static final |
多继承 | 一个类只能继承一个抽象类 | 一个类可以实现多个接口 |
Java8以后,接口中可以有default或者static的方法,当接口中定义了default的方法,就可以在接口中写方法体,子类继承的时候就可以直接拥有该方法。
抽象类可以有抽象方法,其他类不能有抽象方法。
成员变量 | 局部变量 | |
---|---|---|
作用域 | 整个类 | 某个范围内 |
存储与生命周期 | 随着对象的创建而存在,对象的消失而消失,位于堆内存中 | 方法被调用或者语句被执行的时候存在,位于占内存栈帧中 |
初始值 | 有默认初始值,如果被final修饰必须赋值 | 方法体内的使用前必须赋值 |
修饰符 | private、protected、public、static、final | 不能被static修饰 |
静态内部类:定义在类内部的静态类,它可以访问外部类的所有静态变量,而不能访问外部类的非静态变量
class Outer {
private static int index = 1;
static class StaticInner {
void visit() {
System.out.println("index = " + index);
}
}
}
调用visit方法:
Outer.StaticInner inner = new Outer.StaticInner();
inner.visit();
成员内部类:定义在类内部,成员位置上的非静态类
class Outer {
private int index = 1;
class Inner {
void visit() {
System.out.println("index = " + index);
}
}
}
调用visit方法:
//先创建一个outer对象,因为非静态不能直接调用
Outer outer = new Outer();
//然后创建内部类对象
Outer.Inner inner = outer.new Inner();
inner.visit();
局部内部类:定义在方法中的内部类
class Outer {
private int index = 1;
public void functionInner() {
class Inner {
void visit() {
System.out.println("index = " + index);
}
}
}
}
调用visit方法:比较简单:
Outer outer = new Outer();
outer.functionInner();
匿名内部类:没有名字的内部类,开发中用的比较多,一般在方法后面
class Outer implements Inner {
private void test(final int i) {
new Inner() {
@Override
public void visit() {
System.out.println("匿名内部类" + i);
}
}.visit();
}
}
interface Inner {
void visit();
}
//lambda表达式
class Outer implements Inner {
private void test(final int i) {
((Inner) () -> System.out.println("匿名内部类" + i)).visit();
}
}
interface Inner {
void visit();
}
匿名内部类的外部类必须继承一个抽象类或实现一个接口,可以重写父类中的方法以及自定义自己的方法。不能定义任何的静态成员和静态方法。
在多线程编程中,通过实现Runnable接口和Callable接口中可以使用匿名内部类。
当所在的方法形参或者局部变量(JDK1.8前)要被匿名内部类使用时,必须声明final。
方法形参:如果方法执行完,局部变量会被销毁,这样如果匿名内部类仍要使用该局部变量,就会报错。
局部变量:匿名内部类可以访问局部变量,是因为底层将这个局部变量的值传入到了匿名内部类中,并且以匿名内部类的成员变量的形式存在。用final修饰是为了保护数据的一致性,匿名内部类中存入的是一个引用地址,而并不是实际的变量。如果局部变量的引用发生变化,那么匿名内部类是不知道的,其里面还是指向原来的变量,如果程序运行下去可能就会有一些问题。JDK1.8后,Java底层帮我们加上了一个隐式的final。
修饰对象 | 可见范围 | |
---|---|---|
private | 变量、方法、内部类 | 同一类中 |
default | 类、接口、变量、方法 | 同一包中 |
protected | 变量、方法、内部类 | 同一包内的类和所有子类可见 |
public | 类、接口、变量、方法 | 所有类 |
class Person {
private String name;
private int age;
Person(String name, int age) {
this(name); //调用下面的Person(String name)
this.name = name;
this.age = age;
//this(); //直接爆红并提示:this()必须放在构造方法的第一条
}
private Person(String name) {
this();
this.name = name;
}
//如果不写空构造方法,就不能写this(); 方法,直接爆红,因为this本来就是调构造方法的,没有无参构造构造个锤子。
private Person() {}
}
static Person(String name, int age) {
this(name);
this.name = name; //爆红
this.age = age; //爆红
}
private static Person(String name) {
this();
this.name = name; //爆红
}
爆红的原因是这些参数变量不属于任何对象,而是被类的实例所共享(非static不能访问static)
final用于修饰类、属性、方法以及本地变量
加载:将类的class文件读入到内存,并为之创建一个java.lang.Class对象
链接:把类二进制数据合并到JRE中
**验证:用于检验被加载的类是否有正确的内部结构,以确保Class文件的字节流中包含信息是否符合当前虚拟机要求,不会危害虚拟机自身的安全。**其中包括:
准备:负责为类的静态变量分配内存,并设置默认值。
解析:将类的二进制数据中的符号引用替换成直接引用。
初始化:为类的静态变量赋予正确的初始值
初始化的时机(与类加载的时机差不多,对一个类进行主动引用才初始化)
以下情况不会导致初始化:
该阶段与准备阶段的区别:
我们假设有一语句
private static int a = 10;
其执行过程是:字节码加载到内存后,进行完链接的验证,通过后进入准备阶段,该阶段中给static分配内存并赋值为int的默认值0,然后解析,最后进入到初始化阶段把10赋给a。
类加载器:负责加载所有的类,为所有被载入内存中的类生成一个java.lang.Class实例对象,一但一个类被加入JVM中,同一个类就不会被再此载入了。其中一个类以自己的全限定类名作为标识。
ClassLoader父类的loadClass方法用于将类的加载操作委托给其父类加载器去进行,只有该类尚未加载且父类加载器也无法加载该类时,才调用findClass方法。
泛型:允许在定义类、接口、方法时使用类型形参,这个类型形参将在声明变量、创建对象、调用方法时动态地指定(即传入实际的类型参数,也可以称为类型实参)。在没有泛型的情况的下,通过对类型 Object 的引用来实现参数的“任意化”,“任意化”带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的。对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是本身就是一个安全隐患。那么泛型的好处就是在编译的时候能够检查类型安全,并且所有的强制转换都是自动和隐式的。
类型通配符:
C#的List和List是两个不同的类型,在系统运行期生成,有自己的虚方法表和类型数据,这种实现称为类型膨胀,基于这种方法实现的泛型称为真实泛型。
Java的泛型只在程序源码中存在,在编译后的字节码文件中就已经替换为原来的原生类型,并且在相应的地方法插入了强制转型代码。List和List就是同一个类型。这叫类型擦除,是伪泛型。不管为泛型形参传入哪一种类型实参,对于Java来说,它们依然被当成同一个类处理,在内存中也只占用一块内存空间,因此在静态方法、静态初始化块或者静态变量的声明和初始化中不允许使用泛型形参。
public static void method(List integers) {
System.out.println("Integer");
}
public static void method(List integers) {
System.out.println("String");
}
上述代码不能通过编译,原因就是参数List和List编译之后都被擦除了,变成了原生类型List,擦除使得方法签名变得一模一样。
然而下面的代码,在jdk1.5以前是可以通过编译的(1.8无法通过编译):
public static int method(List integer) {
System.out.println("integer");
return 1;
}
public static String method(List integers) {
System.out.println("string");
return "";
}
主要是针对通配符的,Test中的T不能带入int/float等Java默认的基本类型,且T不能是会抛出异常的类。
如果有一个类Test ,里面有一个函数 A f()就不能在写B f()了,因为泛型擦除导致了方法重复。
运行期间无法获取泛型的类型信息,因为泛型都被擦除了,被替换成了原生类型。
Class文件的编译过程中不含传统编译的连接步骤,一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址(直接引用)。这就使得Java方法调用需要在类加载期间,甚至到运行期间才能确定目标方法的直接引用。
静态分派:所有依赖静态类型来定位方法执行版本的分派动作。
public class StaticDispatch {
private static abstract class Human {}
private static class Man extends Human {}
private static class Woman extends Human {}
private void sayHello(Human human) {
System.out.println("hello, human");
}
private void sayHello(Man man) {
System.out.println("hello, man");
}
private void sayHello(Woman woman) {
System.out.println("hello, woman");
}
public static void main(String[] args) {
//Human称为变量的静态类型,Man称为变量的实际类型
//静态类型的变化只在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型是在编译期可知的
//实际类型变化的结构在运行期才可以确定,编译期在编译程序的时候并不知道一个对象的实际类型是什么
Human man = new Man();
Human woman = new Woman();
StaticDispatch sd = new StaticDispatch();
//确定对象为sd的前提下,使用哪个重载版本完全取决于传入参数的数量和数据类型
//编译器在重载时通过参数的静态类型而不是实际类型作为判断依据,静态类型是编译期可知的。
//所以在编译阶段,javac编译器会根据参数的静态类型而不是实际类型作为判断依据
sd.sayHello(man);
sd.sayHello(woman);
}
}
输出结果为:
hello, human
hello, human
静态分派典型应用就是方法重载。静态分派发生在编译阶段,故不是虚拟机执行,而是编译器执行。编译器虽然能确定方法的重载版本,但并不唯一,基本上确认的是一个更加适合的版本。
自动转型是一个自底而上的过程:char-》int-》long-》float-》double。但不会发生char到byte或者short(不安全)
动态分派:在运行期根据实际类型确定方法执行版本的分派过程,这个过程是由虚拟机执行的。
父类与子类之间的多态性,是运行时的多态性,表现在子类对父类的方法进行重新定义。如果在子类中定义某方法与其父类有相同的名称和参数,我们说父类的该方法被重写。子类可以直接拿父类的方法来用,也可以对其进行改造。若子类中的方法与父类中的某一方法有相同的方法名、返回类型和参数表,则新方法将覆盖原有的方法。如果需要使用父类中原有的方法,可以使用super关键字,该关键字引用了当前类的父类。
重写遵循三同一小一大原则:
Java编写的程序一次编译后可以再多个系统平台上运行,原理:Java程序通过JVM在系统平台上运行的,只要该系统可以安装相应的JVM就可以运行Java程序。
Java源代码经过JVM编译器编译后产生的文件(即.class文件)只面向JVM。即机器和编译程序之间加入了一层抽象的虚拟机器,这个虚拟机在任何平台上都提供给编译程序一个共同的接口。编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行。在Java中,这种供虚拟机理解的代码就叫做字节码(.class文件),他不面向特定的处理器,只面向虚拟机。其中,不管程序之前是什么编码形式,在转成.class之后,都是用Unicode编码表示。
**一个Java源代码的执行过程:**Java源代码-》编译器-》JVM可执行的Java字节码(虚拟指令,也就是.class)-》类加载器-》字节码校验器-》JVM中的解释器-》机器可执行的二进制机器码-》程序运行。
Java程序在执行子类的构造方法之前,如果没有用super来调用父类特定的构造方法,则会调用父类的没有参数的构造方法。因此如果父类中只定义了有参的构造方法,而子类构造方法中又没有用super去显示的调用特定的构造方法,则会发生编译错误,因为Java程序在父类中找不到默认的无参构造方法。
maven冲突就是指:依赖时使用maven坐标来定位,而maven坐标主要由gav构成,两个包只要这三个一个不同,maven就认为是不同的。依赖会传递,如果A依赖了B,B依赖了C,那么A的依赖中就会出现B和C。maven对同一个groupId和artifactId的冲突制裁不是以version越大就保留,而是以近的进行保留。且依赖的scope会影响依赖的问题。
方案一:在出现冲突的依赖段里面加入exclusions来让其忽略其依赖的某个包。
<exclusions>
<exclusion>
<artifactId>springartifactId>
<groupId>org.springframeworkgroupId>
exclusion>
exclusions>
方案二:使用dependencyManagement来锁定jar版本
throws用来声明一个方法可能会产生的所有异常,不做任何处理而是将异常抛出。
throw则是用来抛出一个具体的异常类型。
Throwable: 有两个重要的子类:Exception(异常)和 Error(错误),二者都是 Java 异常处理的重要子类,各自都包含大量子类。异常和错误的区别是:异常能被程序本身可以处理,错误是无法处理。
元注解@interface上面需要注解上一些东西,包括@Retention、@Target、@Document、@Inherited四种。
public @interface Init {
//do someting
}
ps:注解的底层实现也是反射。