前言:本人是某双非一本大学的大四学生,目前在复习准备秋招,希望这些自己整理的内容对大家有用。
Java基础复习
1.Java语言有哪些特点?
-
面向对象(封装,继承,多态):耦合度低,易扩展,易复用
封装:javabean的属性设置成私有的,我们对外仅仅暴露通用的get、set去供赋值和获取,而get、set的逻辑是由bean本身设计决定的,外部不能够胡乱的修改,只能按照我们的逻辑去做。从而达到了隐藏信息、保护数据的作用。
继承:什么是什么。比如说猫是动物。继承的好处是代码的复用。
多态:具体表现为一个父类的引用指向子类的实例。提高代码的可扩展性,多态也是很多设计模式的基础。策略模式也是基于多态
多态成员变量,编译运行看左边
多态成员方法,编译看左边,运行看右边
-
跨平台性:一次编写,到处运行。我们的java程序在运行时会被编译成.class字节码文件,我们在不同的平台上安装相应的JVM,JVM负责将.class字节码文件翻译成为指定平台下的机器代码,就可以运行
此处的平台指的是:cpu和操作系统的整体
-
解释与编译并存:Hotspot虚拟机中,既有解释器,也有即时编译器,混合执行。解释器在我们执行到哪段代码就将哪段代码翻译成对应平台的机器码;即时编译器是当我们的虚拟机发现某些代码块或者方法运行得很频繁时,就会将其标记成“热点代码”。【详情看2.运行期优化】
-
支持多线程【详情看java并发】
2.运行期优化
-
在Java8中,默认开启分层编译,-client和-server的设置已经是无效的了。如果只想开启C2,可以关闭分层编译(-XX:-TieredCompilation),如果只想用C1,可以在打开分层编译的同时,使用参数:-XX:TieredStopAtLevel=1
-
发现热点代码的方法有:
- 方法调用计数器:
- 回边计数器:统计循环体循环次数。
-
优化手段:
-
C2编译器会有逃逸分析(-XX:-DoEscapeAnalysis),若无逃逸可能不创建对象节省时间
-
方法内联:发现某些代码是热点代码并且长度不长,会把方法内部的代码拷贝、粘贴到调用者位置【通过参数-XX:+UnlockDiagnosticVMopions -XX:+PrintInlining查看方法内联详情】【-XX:CompileCommand=dontinline,*JIT2.square 禁用对square()方法内联】
-
常量折叠
-
字段优化:【JMH基准测试 @Warmup热身 @Measurement】
-
反射优化:invoke-》【methodAccessor】ma.invoke-》【NativeMethodAccessorImpl】的invoke0() ;if(达到膨胀阈值15,不调用本地方法,调用一个GeneratedMethodAccessor1,新的方法访问器,运行期间动态生成的字节码,用arthas查看)
java -jar arthas-boot.jar
会显示jps;输入进程id对应序号;help可以查看帮助;
jad sun.reflect.GeneratedMethodAccessor1 (反编译类的字节码)
可以看到是直接调用方法
-
3.JVM、JDK、JRE的区别?
- JDK:Java Development Kit;功能齐全,包含了JRE还有前端编译器javac和一些工具(如javadoc)
- JRE:Java Runtime Environment;Java运行时环境。包含了JVM、Java核心类库、java命令
- JVM:Java虚拟机。
4. 什么是字节码?
- 在java中,我们的程序是.java文件,.java文件经过javac编译成.class文件,也就是我们说的字节码文件,字节码文件再经过JVM翻译成指定平台下的机器代码去运行。而字节码和JVM就是Java能够跨平台的关键
5.Java和C++进行比较
- 相同:都是面向对象,都支持封装、继承、多态
- 不同:java类支持单继承,c++支持多继承;但java中接口可以多继承;java有垃圾回收机制自动对内存进行管理,c++需要程序员手动对内存进行管理
6.Java中基本类型
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FuXQ8LxP-1631347270431)(C:\Users\liany\AppData\Roaming\Typora\typora-user-images\image-20210809224904369.png)]
7.标识符和关键字的区别
- 标识符,是我们给一些类、程序、变量、方法取名字,简单说就是名字。而关键字就是一些被Java语言赋予了特殊含义的标识符。
8.常见的关键字
9.continue、break、return的区别
- continue:指的是跳出当前这一次循环,执行下一个循环
- break:指的是跳出整个循环体,执行循环之后的语句
- return:指的是跳出所在的方法,结束方法的运行
10.重写和重载
-
重载:发生在一个类中,多个方法,方法名相同,参数类型不同或个数不同或顺序不同【成为参数列表不同】。(也可以说,同一个类中多个同名方法的参数列表不同,根据传参不同执行不同逻辑)
-
重写:发生在继承,子类继承父类并重写父类的方法,对父类方法进行改造,方法名,参数列表,返回值必须一致,抛出异常小于父类,修饰符范围大于父类。构造方法无法被重写
若父类方法修饰符为private/static/final,则不能够重写,static的方法,子类可以重新声明,【此时只是将父类的方法隐藏】当父类引用指向子类对象时,引用调用方法只会调用父类的静态方法,不具有多态性
11.Java泛型,泛型类型擦除,
-
泛型是JDK5引入的新特性,本质是参数化类型,目的是为了解决类型转换问题
-
但是java中的泛型是伪泛型,因为在Java编译期间,所有的泛型信息都会被擦除。
-
原始类型相等:我们定义两个泛型类型不同的arraylist,让他们去getClass()==比较,结果是true
原始类型:就是擦除了泛型信息之后,最后在字节码中类型变量的真正类型;如果类型变量有限定,那么原始类型就用第一个边界的类型变量类替换【看是否继承,继承了的话就用父类,否则用】
-
通过反射可以添加其他类型元素
-
既然类型擦除了,如何保证我们只能使用泛型变量限定的类型呢?Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。
-
真正设计类型检查的是它的引用,类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。
-
**泛型中的引用传递的问题。**向上转型违背了初衷,向下转型ClassCastException;故都直接编译错误。
-
既然泛型擦除了,为什么我们在获取的时候,不需要进行强制类型转换呢?封装了强转。
-
类型擦除与多态的冲突和解决方法?桥方法,虚拟机巧妙的使用了桥方法,来解决了类型擦除和多态的冲突。如果是我们自己编写Java代码,这样的代码是无法通过编译器的检查的,但是虚拟机却是允许这样做的,因为虚拟机通过参数类型和返回类型来确定一个方法,所以编译器为了实现泛型的多态允许自己做这个看起来“不合法”的事情,然后交给虚拟器去区别。
-
泛型类型变量不能是基本数据类型。不能用类型参数替换基本类型。就比如,没有
ArrayList<double>
,只有ArrayList<Double>
。因为当类型擦除后,ArrayList
的原始类型变为Object
,但是Object
类型不能存储double
值,只能引用Double
的值。 -
instanceof:
ArrayList<String> arrayList = new ArrayList<String>();
因为类型擦除之后,
ArrayList<String>
只剩下原始类型,泛型信息String
不存在了。那么,编译时进行类型查询的时候使用下面的方法是错误的
-
泛型在静态方法和静态类中的问题
泛型类中的静态方法和静态变量不可以使用泛型类所声明的泛型类型参数
举例说明:
public class Test2<T> { public static T one; //编译错误 public static T show(T one){ //编译错误 return null; } }
因为泛型类中的泛型参数的实例化是在定义对象的时候指定的,而静态变量和静态方法不需要使用对象来调用。对象都没有创建,如何确定这个泛型参数是何种类型,所以当然是错误的。
但是要注意区分下面的一种情况:
public class Test2<T> { public static <T >T show(T one){ //这是正确的 return null; } }
因为这是一个泛型方法,在泛型方法中使用的T是自己在方法中定义的 T,而不是泛型类中的T。
if( arrayList instanceof ArrayList<String>)
-
12.通配符
-
泛型的好处就是在编译的时候能够检查类型安全,并且所有的强制转换都是自动和隐式的
-
本质上这些个都是通配符,没啥区别,只不过是编码时的一种约定俗成的东西。比如上述代码中的 T ,我们可以换成 A-Z 之间的任何一个 字母都可以,并不会影响程序的正常运行,但是如果换成其他的字母代替 T ,在可读性上可能会弱一些。通常情况下,T,E,K,V,? 是这样约定的:
- ? 表示不确定的 java 类型
- T (type) 表示具体的一个java类型
- K V (key value) 分别代表java键值中的Key Value
- E (element) 代表Element
-
通配符其实在声明局部变量时是没有什么意义的,但是当你为一个方法声明一个参数时,它是非常重要的对于不确定或者不关心实际要操作的类型,可以使用无限制通配符(尖括号里一个问号,即 <?> ),表示可以持有任何类型。像 countLegs 方法中,限定了上届,但是不关心具体类型是什么,所以对于传入的 Animal 的所有子类都可以支持,并且不会报错。而 countLegs1 就不行。
static int countLegs (List<? extends Animal > animals ) { int retVal = 0; for ( Animal animal : animals ) { retVal += animal.countLegs(); } return retVal; } static int countLegs1 (List< Animal > animals ){ int retVal = 0; for ( Animal animal : animals ) { retVal += animal.countLegs(); } return retVal; } public static void main(String[] args) { List<Dog> dogs = new ArrayList<>(); // 不会报错 countLegs( dogs ); // 报错 countLegs1(dogs); }
-
上界通配符<? extends E>:如果传入的不是E或者E的子类编译不成功;可以直接使用E的方法不用强转
-
下界通配符<? super E>:dst 类型 “大于等于” src 的类型,这里的“大于等于”是指 dst 表示的范围比 src 要大,因此装得下 dst 的容器也就能装 src 。
private <T> void test(List<? super T> dst, List<T> src){ for (T t : src) { dst.add(t); } }
-
? 和 T 的区别:
-
T 是具体的一个java类型,通常用于泛型类和泛型方法的定义,?是一个 不确定 的类型,通常用于泛型方法的调用代码和形参,不能用于定义类和泛型方法。
-
通过 T 来 确保 泛型参数的一致性
// 通过 T 来 确保 泛型参数的一致性 public <T extends Number> void test(List<T> dest, List<T> src) //通配符是 不确定的,所以这个方法不能保证两个 List 具有相同的元素类型 public void test(List<? extends Number> dest, List<? extends Number> src)
-
类型参数可以多重限定而通配符不行
类型参数使用 & 符号设定多重边界(Multi Bounds),对于通配符来说,因为它不是一个确定的类型,所以不能进行多重限定。
-
通配符可以使用超类限定而类型参数不行:类型参数 T 只具有 一种 类型限定方式,但是通配符 ? 可以进行两种限定
T extends A ? extends A ? super A
-
当不知道定声明什么类型的 Class 的时候可以定义一 个Class<?>,而比如反射我们为了避免运行期报ClassCastException,可以如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Izqmg1CX-1631347270439)(C:\Users\liany\AppData\Roaming\Typora\typora-user-images\image-20210810090749130.png)]
-
13.java中只有值传递
- java中只有值传递,对于基本数据类型,在方法体内对参数重新赋值不会改变原有变量的值;对于引用数据类型,在方法体内对参数重新赋予引用,不会改变原有变量持有的引用。基本数据类型传递的是变量的值的拷贝,引用数据类型传的是引用地址的值的拷贝。
14.equals()和==
- ==,双等号两边如果是基本数据类型,比较的是基本数据类型的值,量变如果是引用数据类型,判断的是两个引用所引用的对象地址是否相等
- equals()判断的是两个对象是否相等,它不能用于比较基本数据类型,是Object的方法,若不重写其中的逻辑就是和双等一致,因为我们要用equals()来判断对象是否相等,故常会覆盖equals(),
15.hashCode()和equals()
- 相同:都是Object的函数
- equals():不能用于基本数据类型的比较,用于判断两个引用类型变量引用对象是否相等,默认逻辑是==,【提一下双等】故我们常常会去覆盖。
- hashCode():作用是获取哈希码,也称为散列码,实际上是返回一个int整数,用于确定对象在哈希表中的索引位置。虽然每个类都有hashCode(),但仅在我们某个创建类的哈希表时,这个方法才产生作用【本质是散列表的类:HashMap、HashTable、HashSet】,除此之外,hashCode()还是一个本地方法。
- 0)为什么要有hashCode():用HashSet如何检查重复来说明
- 大大减少equals的次数,提高执行速度。具体执行逻辑是:
- 当我们要向HashSet中添加一个对象时,我们会先去调用hashCode计算出对象的索引,如果没有该索引上没有其他对象,则成功加入;如果有则调用equals去比较,相等时直接加入失败,不等时再如何解决哈希冲突???!!!!!!!。【】
- 1)为什么重写了equals()就得重写hashCode():由于默认的hashCode()方法是根据对象的内存地址经过哈希算法得来,那么此时两个对象相等,却无法保证hashCode()的结果一致,这和hashCode()的规则相违背–如果两个对象相等,其hashCode相等。因此我们要重写hashCode()方法,保证两个相等的对象哈希码一致。
- 2)为什么不同对象可能会有相同的hashCode:与我们的算法设计有关。但避免不了,长度固定。
16.什么是散列表
- 散列表存储的是键值对,特点是我们能够根据键快速检索出对应的值。
17.自动拆箱与自动装箱
-
装箱:将基本数据类型用其对应的包装类型包装起来
-
拆箱:将包装类型转换为基本数据类型
-
比如Integer,拆箱时调用的是intValue(),装箱时调用的时valueOf()
- 当 "=="运算符的两个操作数都是 包装器类型的引用,则是比较指向的是否是同一个对象,而如果其中有一个操作数是表达式(即包含算术运算,因为包装类型不支持运算)则比较的是数值(即会触发自动拆箱的过程);另外,对于包装器类型,equals方法并不会进行类型转换
-
包装类型的缓存:
- Byte、Integer、Long、Short都是-128-127
- Character:0-127
- Double和Float个数不确定无法缓存
-
18.自动类型转换(隐式类型转换)
- 若参与运算的数据类型不同,则先转换成同一类型,然后进行运算。
- 转换按数据长度增加的方向进行,以保证精度不降低。如果一个操作数是long型,计算结果就是long型;如果一个操作数是float型,计算结果就是float型;如果一个操作数是double型,计算结果就是double型。例如int型和long型运算时,先把int量转成long型后再进行运算。
- 所有的浮点运算都是以双精度进行的,即使仅含float单精度量运算的表达式,也要先转换成double型,再作运算。
- char型和short型参与运算时,必须先转换成int型。
- 在赋值运算中,赋值号两边的数据类型不同时,需要把右边表达式的类型将转换为左边变量的类型。如果右边表达式的数据类型长度比左边长时,将丢失一部分数据,这样会降低精度
19.深拷贝与浅拷贝
- 浅拷贝:对基本数据类型进行值传递,对引用数据类型传递引用地址。
- 深拷贝:对基本数据类型进行值传递,对引用数据类型,开辟新的内存空间创建新对象,并赋值原来的内容。【可以通过序列化和反序列化快速实现深拷贝,但要注意transient】
20.在Java中定义一个不做事且没有参数的构造方法的作用
- 执行子类的构造方法之前,如果没有用super()来调用父类特定的构造方法,则会调用父类的无参数构造方法,如果没有定义这个无参构造方法,会报错。
21.关于构造器
- 构造器可以重载(Overload)但不可以重写(Override)
- 在Java中定义一个不做事且没有参数的构造方法的作用:执行子类的构造方法之前,如果没有用super()来调用父类特定的构造方法,则会调用父类的无参数构造方法,帮助子类初始化,如果没有定义这个无参构造方法,会报错。
22.修饰符
22.1.静态方法调非静态成员为何非法?
- 因为静态方法不可以通过对象调用,所以静态方法里不能够访问非静态变量
22.2.对于static、final、this、super的总结
-
final:
- 修饰变量:对于一个final修饰的变量,如果是基本数据类型,则其数值在一旦初始化之后便不可变;如果是引用类型数据,则对其初始化之后不能让其指向另一个对象
- 修饰类:表明一个类不可以被继承,并且这个类中的所有方法都被隐式指定为final修饰
- 修饰方法:防止被子类重写。
-
static:
- 修饰成员变量和方法:被static修饰的变量和方法属于类,并且建议通过类名调用,静态变量存在方法区
- 静态代码块:静态代码块定义于方法之外,执行顺序是(静态代码块–>非静态代码块–>构造方法),类不管创建多少对象,静态代码块只执行一次
- 静态导包(了解)
-
this:主要作用是提高可阅读性
-
super:在子类方法中调用父类的方法或者构造器;如果在子类构造器中调用super()必须在第一行否则报错
this、super不能出现在静态方法中
23.接口和抽象类的区别
-
抽象类中方法可以是抽象的也可以是非抽象的,成员变量可以是变量也可以是常量
在JDK7以及7以前,接口中的方法必须是抽象的,变量必须是static、final的。
JDK8以后,接口也可以定义静态方法和默认方法。
JDK9,接口允许定义私有方法
-
从设计层面来看,抽象类是对类的抽象,是一种模板的设计;接口时对行为的抽象,是行为的规范。
24.String
24.1.为什么String是不可变的?
- 在JDK1.8,String的底层其实是一个char[] value【JDK9后改为byte[] value】,而这个引用类型的成员变量是用private final修饰的,final修饰说明它的引用地址不可变,用private修饰的同时我们不对外提供这个成员变量的set方法,外界无法对其进行修改,因此是不可变的。【可用反射破坏】
24.2.为什么String要设计成不可变?
-
字符串常量池的需要:我们知道,在Java中有一个特殊的存储区域StringTable,当我们创建一个字符串对象时,如果说这个字符串值在常量池中已经存在了,会去引用已经存在的对象。那么如果说我们允许字符串改变,那么很可以一个对象的改变会影响到另一个独立的对象,因此我们设计成不可变的。
-
允许String对象缓存hashCode:Java程序中我们的String对象的哈希码经常被使用,而字符串的不变性保证了每个字符串对象的哈希码不变,因此我们在String中声明了一个私有变量hash来缓存哈希码,优化性能。
引出一个问题:为什么要有字符串常量池?因为我们的程序中字符串是经常使用的,通过字符串常量池提高复用率,减少内存的使用。
24.3.一些字符串拼接的操作
- 文字说明:
- 常量和常量的拼接结果在常量池中,原理是编译器优化【Javac认为是常量,不会改变,编译器已经可以确定下来】
- 只要其中有一个是变量,拼接的结果就在堆中,变量拼接的原理是StringBuilder【JDK5以前用的是StringBuffer】,根据实际情况会有中间匿名对象创建,且最终结果仅在堆中,不会在常量池中创建。
24.4.StringTable的其他细节
- 底层是用HashTable实现,在JDK6中固定长度1009,JDK7中默认长度是60013,JDK开始长度最小值是1009,默认是60013。通过-XX:StringTableSize可以修改
24.5.String真的不可变吗?通过反射去改变指向的数组对象即可。
String s = "Hello world!";
Field valueOfString = String.class.getDeclaredField("value");
valueOfString.setAccessible(true);
char[] value = (char[]) valueOfString.get(s);
value[5] = "_";
System.out.println(s);//Hello_world
24.6.intern()
intern 方法:从字符串常量池中查询当前字符串是否存在,如果常量池中存在当前字符串, 就会直接返回当前字符串,如果常量池中没有此字符串,会将此字符串放入常量池中后, 再返回
- JDK6会在常量池创造对象
- JDK7后,常量池已经从方法区移到堆中,会在堆中没有这个对象时创建对象,堆中有的时候,在常量池中直接存储堆中相应字符串对象引用,不会重新创对象。
24.7.StringBuilder、StringBuffer
- StringBuilder、StringBuffer都继承自AbstractStringBuilder,底层也都是char[] value,但是没有用final修饰,所以是可变的;StringBuffer对方法加了同步锁,所以是线程安全的,StringBuilder没有加同步锁,不是线程安全的。
- 如果操作少量数据用String,是单线程操作大量数据建议用StirngBuilder,多线程操作大量数据建议用StringBuffer【引出线程安全问题】
25.Object常用方法
26.Java序列化中如果有些字段不想进行序列化,怎么办
- 可以使用transient关键字修饰;transient的作用:阻止被transient修饰的变量序列化,当对象被反序列化时,被transient修饰的变量值不会恢复。但transient只能修饰变量,不能修饰类和方法。
27.获取用键盘输入常用的两种方法
-
使用Scanner
Scanner input = new Scanner(System.in);String s = input.nextLine();input.close;
-
使用BufferedReader
BufferedReader input = new BufferedReader(new InputStreamReader(System.in));String s = input.readLine();
28.Collections工具类和Arrays工具类常用方法总结
-
Collections:排序、查找、替换、同步控制
void reverse(List list)//反转 void shuffle(List list)//随机排序 void sort(List list)//按自然排序的升序排序 void sort(List list, Comparator c)//定制排序,由Comparator控制排序逻辑 void swap(List list, int i , int j)//交换两个索引位置的元素 void rotate(List list, int distance)//旋转。当distance为正数时,将list后distance个元素整体移到前面。当distance为负数时,将 list的前distance个元素整体移到后面。 / int binarySearch(List list, Object key)//对List进行二分查找,返回索引,注意List必须是有序的 int max(Collection coll)//根据元素的自然顺序,返回最大的元素。 类比int min(Collection coll) int max(Collection coll, Comparator c)//根据定制排序,返回最大元素,排序规则由Comparatator类控制。类比int min(Collection coll, Comparator c) void fill(List list, Object obj)//用指定的元素代替指定list中的所有元素。 int frequency(Collection c, Object o)//统计元素出现次数 int indexOfSubList(List list, List target)//统计target在list中第一次出现的索引,找不到则返回-1,类比int lastIndexOfSubList(List source, list target). boolean replaceAll(List list, Object oldVal, Object newVal)//用新元素替换旧元素 / synchronizedXXX()
-
Arrays:
排序 : sort() 查找 : binarySearch() 比较: equals() 填充 : fill() 转列表: asList() 转字符串 : toString() 复制: copyOf()
29.Comparator&&Comparable
-
Comparator和Comparable的区别
一个类实现了Camparable接口则表明这个类的对象之间是可以相互比较的,这个类对象组成的集合就可以直接使用sort方法排序。
Comparator可以看成一种算法的实现,将算法和数据分离,Comparator也可以在下面两种环境下使用:- 类的设计师没有考虑到比较问题而没有实现Comparable,可以通过Comparator来实现排序而不必改变对象本身
- 可以使用多种排序标准,比如升序、降序等
-
总结一下,两种比较器Comparable和Comparator,后者相比前者有如下优点:
- 如果实现类没有实现Comparable接口,又想对两个类进行比较(或者实现类实现了Comparable接口,但是对compareTo方法内的比较算法不满意),那么可以实现Comparator接口,自定义一个比较器,写比较算法
- 实现Comparable接口的方式比实现Comparator接口的耦合性要强一些,如果要修改比较算法,则需要修改Comparable接口的实现类,而实现Comparator的类是在外部进行比较的,不需要对实现类有任何修改。从这个角度说,实现Comparable接口的方式其实有些不太好,尤其在我们将实现类的.class文件打成一个.jar文件提供给开发者使用的时候。实际上实现Comparator 接口的方式后面会写到就是一种典型的策略模式.