JAVA基础
用于JAVA复习
1. static静态变量
- 静态变量:用static修饰的变量
- 实例变量:没有用static修饰的变量
- 二者的区别:静态变量是随着类加载时被初始化完成的,在内存中仅有一个,且JVM只会为它分配一次内存,同时该类所有的实例都共享静态变量,可以直接通过类名.静态变量名来访问它;而每创建一个实例就会产生一个实例变量,实例变量的生命周期与该实例相同。
一般在对象之间需要共享数据,或者需要在没有创建对象的情况下需要进行调用方法或变量,那么就可以通过static关键字来修饰
static可以修饰属性,方法和代码块
- static修饰属性(类变量),那么该属性就可以用类名.属性名来访问,那么该属性就是该类的类变量,为本类对象所共有,它只和类有关,与对象无关。
- static修饰方法(静态方法),那么该方法成为该类所共有的方法,可以用类名.方法名进行访问。
- static修饰初始代码块,该代码块叫做静态初始代码块,这种代码块只在类加载时被执行一次。可以用静态初始代码块初始化一个类。
- static只能修饰内部类,正常的外部类不能使用static修饰。
2. JDK,JRE和JVM的联系区别
- JDK提供编译、调试和运行功能,用于开发,包含了JRE和JVM
- JRE提供了运行JAVA程序的平台,包含了JVM
- JVM是JAVA的虚拟机,提供了内存管理、垃圾回收和安全机制等
3. 堆和栈的区别,类和对象
- 堆内存:保存的是对象的具体信息,在程序之中堆内存空间是通过new开辟的
- 栈内存:保存的是一块堆内存的地址,可以通过地址找到堆内存,然后找到对象内容
类是对某一类事物的共性的抽象概念,对象描述的是一个具体的产物
类是一个模板,而对象才是类可以使用的实例,先有类再有对象
4. 垃圾空间
垃圾空间指的是没有任何栈内存所指向的堆内存空间,所有的垃圾将被GC(Garbage Collector)垃圾收集器定期进行回收并且释放无用内存,但是如果垃圾空间过多,则会影响到GC的处理性能,从而降低整体的程序性能。
5. 请解释String比较中”==”和equals()的区别?
- ==:如果是基本数据类型,进行的是数值比较;如果是引用类型,则比较的是两个内存的地址
- equals():是String类提供的一个比较方法,可以直接进行字符串内容的判断,区分大小写
1 | // 直接赋值过程中,内存池数据自动保存,如果有相同数据定义是可以减少对象的产生 |
- 是否区分大小写判断
1 | // 区分大小写 |
- String类的两种对象实例化方式
1 | // 直接赋值,只会产生一个实例化对象,并且可以自动保存到对象池中,以实现该字符串实例的重用 |
- 下面这种字符串修改方式会带来大量的垃圾内存
1 | String str = "aa"; |
6. 主方法分析
public static void main(String args[])
- public:描述的是一种访问权限,主方法是一切的开始点,开始点一定是公共的
- static:程序的执行是通过类名称完成的,表示此方法可以由类直接调用
- String args[]:字符串的名称,可以实现程序启动参数的接收
7. 请解释super与this的区别
- 在程序类中使用this表示先从本类查找所需要的属性或方法,如果本类中不存在则查找父类定义,如果使用super则表示不查找子类,直接查找父类
- this与super都可以进行构造方法的调用,this()调用的是本类构造,super()调用的是父类构造,两者都必须放在构造方法的首行,所以不能同时出现
- this可以表示当前对象
8. JAVA三大特性
封装、继承、多态
- 封装:指隐藏对象的属性和实现细节,仅对外提供公共访问方式。将成员变量私有化,对外提供对应的set、get方法对其进行访问,可以提供对数据访问的安全性
- 继承:继承是面向对象最显著的一个特性。继承是从已有的类中派生出新的类,新的类能吸收已有类的数据属性和方法,并能扩展新的能力。在JAVA中,被继承的类叫父类或超类,继承父类的类叫子类或派生类
- 多态:在面向对象语言中,多态性是指一个方法可以有多种实现版本,即“一种定义,多种实现”。利用多态性可以设计和实现可扩展的系统,只要新类也在继承层次中。新的类对程序的通用部分只需进行很少的修改,或不做修改。类的多态性表现为方法的多态性,方法的多态性主要有方法的重载和方法的重写
9. 构造方法
主要目的:可以通过构造方法实现实例化对象中的属性初始化处理,相当于依次调用了set方法
要求如下:
- 构造方法名必须与类名保持一致
- 构造方法不允许设置任何的返回值类型
- 构造方法实在使用关键字new实例化对象的时候自动调用的
结论:一个类至少存在一个构造方法,若没有手动构造,则存在一个默认的无参构造方法
问题:为什么构造方法上不允许设置返回值类型?- 如果在构造方法上使用了void,那么此构造方法就与普通方法的结构完全相同了,这样编译器会认为此方法是一个普通方法
普通方法与构造方法最大的区别?- 构造方法是在类对象实例化的时候调用的,而普通方法是在类对象实例化之后调用的
10. 多态性
多态是同一个行为具有多个不同表现形式的能力
多态性:指允许不同子类型的对象对同一消息做出不同的响应,也可以说是用同样的对象调用同样的方法,但是做了不同的事。多态性分为编译时的多态性和运行时的多态性。
方法重载(overload)实现的是编译时的多态性(也称为前绑定),方法重写(override)实现的是运行时的多态性(也称为后绑定)
要实现多态需要做两件事:(1)方法重写(子类继承父类并重写父类中已有的或抽象的方法);(2)对象造型(用父类型引用子类型对象,这样同样的引用调用同样的方法就会根据子类对象的不同而表现出不同的行为)
在JAVA中对于多态性有两种实现的模式
1. 方法的多态性
- 方法的重载
- 方法的重写
2. 对象的多态性- 父子实例之间的转化处理,它有两种模式
- 对象向上转型
父类 父类实例 = 子类实例;
向上转型,也就是创建一个子类对象,把它当作父类来看待,向上转型一定是安全的。但是一但向上转型为父类对象,就不能调用子类原本特有的方法- 对象向下转型
子类 子类实例 = (子类)父类实例
instanceof关键字:可以用来判断某个实例是否是某个类的对象
11. 重写和重载是什么意思?
重写(Override)是指子类对父类方法的一种重写,只能比父类抛出更少的异常,访问权限不能比父类的小,被重写的方法不能是private的,否则只是在子类中重新定义了一个新方法
重载(Overload)表示同一个类中可以有多个名称相同的方法,但这些方法的参数列表各不相同
- 构成重载的条件有哪些?
参数类型不同、参数个数不同、参数列表顺序不同
- 重写原因
子类与父类一但产生继承关系之后,实际上子类会继承父类中的全部定义,但是这里面也有可能出现不合适的地方,子类如果发现父类中设计不足并且需要保留父类中的方法或属性名称的情况下就会发生重写
- 一般认为JAVA内参数的传递都是值传递,JAVA中实例对象的传递都是引用传递
区别 Overloading Override 中文含义 重载 重写 概念 方法名称相同,参数的类型及个数不同 方法名称、参数类型、个数及返回值相同 权限 没有权限限制 被重写方法不能拥有比父方法更严格的控制权限 范围 发生在一个类中 发生在继承关系类中
12. Object类
在JAVA中只有一个类是不存在有继承关系的,这个类就是Object,Object是所有类的父类
在Object类设计的时候考虑到了所有的继承问题,所以该类提供有无参构造方法,所以子类不会出现构造方法调用失败的语法错误
如果一个程序的方法要求可以接收所有类对象的时候就可以利用Object实现处理
Object类对象可以接受所有的数据类型,包括基本数据类型、类对象、接口对象、数组
获取对象信息:toString()toString()是Object类提供的一个方法,可以获取一个对象的完整信息
可在对象类中直接重写此方法
1
2
3
4
5
6 Object fun(Object a, Object b)
{
if(a instanceof Integer && b instanceof Integer)
return (Integer)a + (Integer)b;
return null;
}
13. 抽象类
基本定义:抽象类的主要作用是对子类中的重写方法进行约定,在抽象类里面可以去定义一些抽象方法以实现这样的约定,抽象方法指的是使用了abstract关键字定义的并且没有提供方法体的方法,而抽象方法所在的类必须为抽象类。抽象类必须使用abstract关键字来定义(在普通类的基础上追加抽象方法就是抽象类)
当一个抽象类定义完成之后(切记:抽象类不是完整的类),如果想要去使用抽象类,则要满足以下条件
- 抽象类必须提供有子类,子类使用extends继承一个抽象类
- 抽象类的子类(不是抽象类)一定要重写抽象类中的全部抽象方法
- 抽象类的对象实例化可以利用对象多态性,通过子类向上转型的方式完成
注:抽象类自己无法直接实例化
- 抽象类中允许没有抽象方法,但即便没有抽象方法,也无法直接使用关键字new实例化抽象类对象
- 抽象类中可以有static方法,并且该方法不受到抽象类对象的局限,可以通过类名调用
- 在定义抽象类的时候绝对不能使用final关键字来进行定义,因为抽象类必须有子类,而final定义的类是不能有子类的
抽象类最大的好处:(1) 对子类方法的统一管理;(2) 可以自身提供一些普通方法调用抽象方法(这些抽象方法必须在有子类提供实现的时候才会生效)
14. 接口
接口使用原则
- 接口需要被子类实现(implements),一个子类可以实现多个父接口
- 子类(如果不是抽象类),那么一定要重写接口之中的全部抽象方法
- 接口对象可以利用子类对象的向上转型进行实例化
注:由于接口描述的是一个公共的定义标准,所有在接口之中的抽象方法的访问权限都是public
特点:全部由抽象方法和全局常量组成
15. 抽象类和接口的区别
区别 抽象类 接口 定义关键字 abstract interface 组成 构造方法、普通方法、static方法、全局常量、普通成员变量 抽象方法、全局常量 权限 可以使用各种权限定义 只能使用public,默认使用 子类使用 子类通过extends可以继承一个抽象类 子类通过implements可以实现多个接口 两者关系 抽象类可以实现若干个接口 接口不允许继承抽象类,但是允许继承多个父接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 interface t1
{
void t1();
}
interface t2
{
void t2();
}
interface t3 extends t1, t2
{
void t1();
void t2();
}
- 抽象类可以有构造方法,接口不能有构造方法
- 抽象类可以有普通成员变量,接口没有普通成员变量
- 抽象类可以包含非抽象的普通方法,接口中所有方法必须都是抽象的,不能有非抽象的普通方法
- 抽象类中的抽象方法的访问权限可以是public,protected,接口中的抽象方法只能是public类型的,并且默认为public abstract
- 抽象类中可以包含静态方法,接口中不能包含静态方法
- 抽象类和接口中都可以包含静态成员变量,抽象类中的静态成员变量的访问类型可以任意,但接口中定义的变量只能是public static final类型,并且默认为public static final类型
共同点:
- 抽象类或接口必须定义子类
- 子类一定要覆盖抽象类或接口中的全部抽象方法
- 通过子类的向上转型实现抽象类或接口对象实例化
注:当抽象类和接口都可以使用的情况下优先考虑接口,因为接口可以避免子类的单继承局限
为什么MVC中使用接口不使用抽象类?
因为一个类可以实现多个接口,但只能继承一个抽象类,使用接口有利于扩展和解耦合。接口更像是一个功能插件,类需要实现什么功能就装上什么插件;而抽象类则是一些事物共同的方法和属性的定义
16. 泛型
出现原因:在没有泛型的情况下,通过对Object的引用来实现参数的“任意化”,其带来的缺点是要做显示的强制类型转换,而这种转换要求开发者对实际参数类型可以预知的情况下进行。对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这本身就是一个安全隐患
本质:把类型明确的工作推迟到创建对象或调用方法的时候采取明确类型
注:泛型之中只允许设置引用类型,如果要操作基本类型必须使用包装类
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 public class ServiceUtil
{
private final static BorrowService BORROW_SERVICE;
private final static UserService USER_SERVICE;
private final static StudentService STUDENT_SERVICE;
private final static BookService BOOK_SERVICE;
private static final Map<Class<?>, Service> map = new HashMap<>();
static
{
BORROW_SERVICE = new BorrowServiceImpl();
USER_SERVICE = new UserServiceImpl();
STUDENT_SERVICE = new StudentServiceImpl();
BOOK_SERVICE = new BookServiceImpl();
map.put(BorrowService.class, BORROW_SERVICE);
map.put(UserService.class, USER_SERVICE);
map.put(StudentService.class, STUDENT_SERVICE);
map.put(BookService.class, BOOK_SERVICE);
}
public static <T> T getService(Class<T> tClass)
{
return tClass.cast(map.get(tClass));
}
}使用泛型可以解决大部分的类对象的强制转换处理,避免了对象的向下转型处理
好处:
- 代码更加简洁,不用强制转换
- 程序更加健壮,只要编译时期没有警告,那么运行时期就不会出现ClassCastException异常
- 可读性和稳定性
增强for遍历集合
在创建集合时,明确了集合的类型,可以使用增强for来遍历集合
1
2
3
4
5
6
7
8
9 ArrayList<String> list = new ArrayList<String>(){{
this.add("a");
this.add("b");
this.add("c");
}};
for(String s : list)
{
System.out.println(s);
}通配符
通配符“?”只能调用与对象无关的方法,不能调用对象与类型有关的方法
1
2
3
4
5 void fun(ArrayList<?> list)
{
list.add("1"); // 错误,因为不知道传进来的list实际上的类型是什么
System.out.println(list.size()); // 可以运行,因为size()方法与类型无关
}?和T都表示不确定的类型,区别在于可以对T进行操作,但是不能对?进行操作
1
2 List<T> list = new ArrayList<T>(); // 指定集合元素只能是T类型
List<?> list = new ArrayList<?>(); // 集合元素可以是任意类型,一般是在方法中,实施未来说明用法总结:T是一个确定的类型,通常用于泛型类和泛型方法的定义,?是一个不确定的类型,通常用于泛型方法的调用代码和形参,不能用于定义类和泛型方法
在?通配符的基础上还提供有两类小的通配符
- ? extends 类名:设置泛型的上界
例如:定义“? extends Number”,表示该泛型类型只允许设置Number或Number的子类
List<? extends Number> list = new ArrayList<>();
- ? super 类名:设置泛型的下界
例如:定义“? super Number”,表示只能够使用Number或Number的父类
```javavoid fun(ArrayList<? super Number> list)
{
System.out.println(list.size());
}
ArrayListlist1 = new ArrayList<>();
fun(list1);> **区别:上界通配符主要用于读数据,下界通配符主要用于写数据**
17. 包装类
包装原因:Object类最大的特点是它是所有类的父类,并且可以接收所有的数据类型,但是在这个过程中有个问题:基本数据类型并不是一个类,所以如果想将基本数据类型以类的形式进行处理,就需要对其进行包装
JAVA中的基本数据类型没有方法和属性,而包装类就是让基本数据类型有了方法和属性,实现对象化交互
- 装箱:基本数据类型转换为包装类
- 拆箱:包装类转换为基本数据类型
一些方法:
- Integer.toString()可以将整型转换为字符串
- Integer.parseInt()可以将字符串转换为整型
- 通过ValueOf()方法可以将数据对象转换为调用该函数的数据对象
1
2
3
4 int a = 123;
String s = Integer.toString(a);
int b = Integer.parseInt(s);
Integer integer = Integer.valueOf(s);
18. 枚举
主要作用是用于定义有限个对象的一种结构,枚举属于多例设计
使用enum关键字来定义枚举类型,其中FRONT、BEHIND、LEFT、RIGHT都是枚举项,它们都是本类的实例,本类一共就只有四个实例对象
1
2
3
4 public enum Direction{
FRONT, BEHIND, LEFT, RIGHT("右");
}
Direction d = Direction.FRONT;不能使用new来创建枚举类的对象,因为枚举类的实例对象就是枚举类中的枚举项,在类外使用类名.枚举项进行访问
枚举与switch
在switch中,不能使用枚举类名称,例如 “case Direction.FRONT: ”,这是错误的,编译器会根据switch中d的类型来判断每个枚举类型,在case中必须直接给出与d相同类型的枚举选项,而不能再有类型
1
2
3
4
5
6
7
8
9 Direction d = Diretion.FRONT;
switch(d)
{
case FRONT: break;
case BEHIND: break;
case LEFT: break;
case RIGHT: break;
default: break;
}请解释enum与Enum的区别?
- enum:是从JDK1.5之后提供的一个关键字,用于定义枚举类
- Enum:是一个抽象类,所有使用enum定义的类就默认继承了此类
19. 异常
JAVA异常处理流程
出现异常之后将终止整个程序,为了保证程序出现了非致命错误之后依然可以正常完成,所以需要一个完善的异常处理机制处理异常
在JAVA之中如果要进行异常的处理,可以使用:try、catch、finally这几个关键字来完成,其基本的结构如下:
1
2
3
4
5
6
7
8
9
10
11 try{
// 可能出现异常的语句
} catch(异常类型 异常对象){
// 异常处理
} catch(异常类型 异常对象){
// 异常处理
} ...(异常类型 异常对象){
// 异常处理
} finally{
// 不管异常是否处理都要执行
}在此格式之中可以使用的组合为:try···catch、try···catch···finally、try···finally
如果想要获得非常完整的异常信息,则可以使用异常类中提供的printStackTrace()方法
对于异常的处理格式也可以在最后追加一个finally程序块,表示异常处理后的出口,不够是否出现异常都会被执行
注意:
- 即便有了异常处理语句,但是如果没有进行正确的异常捕获,那么程序也会导致中断(finally中的代码依旧会被执行),这样的情况应该扩大异常捕获的范围
- 在进行多个异常同时处理的时候要把范围大的异常放在捕获范围小的异常之后
JAVA程序中可以处理的异常的最大类型就是Throwable,Throwable有两个子类:- Error:此时程序还未执行出现的错误,开发者无法处理
- Exception:程序中出现的异常,开发者可以处理
throws关键字
- 使用throws关键字来进行异常类型的标注,必须有对应的try···catch处理
- throws表示出现异常的一种可能性,并不一定会发生这些异常
- 异常处理是会一层层往上抛的,直到遇到某个方法处理了这个异常或者最后抛给JVM进行处理
如果主方法继续向上抛出异常,就表示此异常将交由JVM处理
1
2
3
4
5
6
7
8
9
10
11
public void test1(){
try{
System.out.println(div(1, 2));
}catch(Exception e){
e.printStackTrace();
}
}
public int div(int x, int y) throws Exception {
return x / y;
}throw关键字
表示手动进行异常的抛出,即:此时将手动产生一个异常类的实例化对象,并进行异常的抛出处理
1
2
3
4
5
6
7
8
public void test1(){
try{
throw new Exception("发生异常");
}catch(Exception e){
e.printStackTrace();
}
}面试题:
请解释throw与throws的区别?
- throw:是在代码块中使用的,主要是手动进行异常对象的抛出
- throws:是在方法定义上使用的,表示将在此方法中可能产生的异常明确告诉给调用者,由调用者进行处理
- throws表示出现异常的一种可能性,并不一定会发生这些异常;执行throw则一定会抛出某种异常
请解释RuntimeException与Exception的区别?常见的RuntimeException?
- RuntimeException是Exception的子类
- RuntimeException标注的异常可以不需要进行强制性try···catch处理,而Exception异常必须进行强制性处理
- 常见的RuntimeException异常:NumberFormatException、ClassCastException、NullPointException…
1
2
3
4
public void test1(){
throw new RuntimeException("发生异常");
}自定义异常类
分为继承Exception和RuntimeException两种,如果继承的是Exception,则需要进行try···catch处理
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 static class ExceptionTest extends Exception{
public ExceptionTest(String msg){
super(msg);
}
}
static class RuntimeExceptionTest extends RuntimeException{
public RuntimeExceptionTest(String msg){
super(msg);
}
}
>
public void test1(){
System.out.println(div1(2, 1));
try{
System.out.println(div2(2, 1));
}catch(Exception e){
e.printStackTrace();
}
}
public int div1(int x, int y) throws RuntimeExceptionTest {
return x / y;
}
public int div2(int x, int y) throws ExceptionTest {
return x / y;
}assert断言
从JDK1.4开始有断言的功能,确定代码执行到某行之后一定是期望的结果。但断言不一定是准确的,也可能出现偏差,但是这种偏差不应该影响程序的正常执行
1
2
3
4
5
6
void test2(){
int a = 10;
assert a == 100 : "a的内容不是100";
System.out.println(a);
}如果想要执行断言,则必须在程序执行的时候加入参数:java -ea 类名
所以在JAVA中并没有将断言设置为程序必须执行的步骤,需要在特定环境下才可以开启
20. 内部类
Java内部类详解 - Matrix海子 - 博客园 (cnblogs.com)
将一个类定义在另一个类里或者方法里,这样的类被称为内部类成员内部类
1
2
3
4
5
6
7
8
9
10 class Student{
private String name;
private int age;
class tcl{
void fun(){
System.out.println(Student.this.name);
}
}
}
优点:轻松的访问外部类中的私有属性
缺点:破坏了程序的结构
注意:内部类可以轻松访问外部类的私有属性,同理外部类也可以轻松访问内部类中的私有成员和私有方法
使用了内部类后,内部类与外部类之间的私有操作的访问就不再需要通过setter、getter以及其他的间接方式完成了,可以直接进行处理操作
内部类本身也属于一个类,虽然大部分的情况下内部类往往是被外部类包裹的,但是外部依然可以产生内部类的实例化对象,此时实例化格式如下:
外部类名.内部类名 内部类对象 = new 外部类().new 内部类();
1 Student.tcl tmp = new Student().new tcl();静态内部类
如果在内部类上使用static定义,那么这个内部类就变为了“外部类”,static定义的都是独立于类的一个结构,所以该类结构就相当于是一个独立的程序类了
注意:static定义的不管是类还是方法只能够访问static成员,所以static定义的内部类只能访问外部类中的static属性和方法
1
2
3
4
5
6
7
8
9
10
11 class Student
{
private static int age = 18;
static class Inner
{
void fun()
{
System.out.println(Student.age);
}
}
}这个时候的Inner类是一个独立的类,此时想要实例化Inner类对象,只需要根据“外部类.内部类”的结构实例化对象即可,格式如下:
外部类名.内部类名 内部类对象 = new 外部类.内部类()
1
2
3
4
5
6
7 public class test
{
public static void main(String[] args)
{
Student.Inner tmp = new Student.Inner();
}
}方法内部类
内部类可以直接访问外部类的私有属性,也可以直接访问方法中的参数,但是对于方法中的参数直接访问是从JDK1.8开始支持的;在JDK1.8之前,如果方法中定义的内部类要想访问方法中的参数,则参数前必须要追加final
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 static class Student
{
private String name;
private int age;
void fun(String msg)
{
class tcl
{
void test()
{
System.out.println(msg);
}
}
new tcl().test();
}
}
void test2()
{
new Student().fun("a");
}匿名内部类
匿名内部类是一种简化的内部类处理形式,其主要是在抽象类和接口的子类上使用的,与内部类相比,匿名内部类没有名字,只能够使用一次并且结构固定的一个子类操作
1
2
3 interface IMessage{
public void send(String msg);
}
1
2
3
4
5
6
7
8
9
10
11
12
void test3()
{
IMessage iMsg = new IMessage(){
public void send(String msg)
{
System.out.println(msg);
}
};
iMsg.send("hello");
}相关操作可以用lambda表达式简化
21. 数据类型
JAVA中的数据类型分为两大类,基本数据类型和引用数据类型
基本数据类型
- 整数类型:long、int、short、byte
- 浮点类型:float、double
- 字符类型:char
- 布尔类型:boolean
| 数据类型 | 大小/位 | 可表示数据范围 | 默认值 || :——————: | :—–: | :——————————————: | :———-: |
| byte(字节型) | 8 | -128~127 | 0 |
| short(短整型) | 16 | -32768~32767 | 0 |
| int(整型) | 32 | -2147483648~2147483647 | 0 |
| long(长整型) | 64 | -9223372036854775808~9223372036854775807 | 0 |
| float(单精度) | 32 | -3.4E38~3.4E38 | 0.0 |
| double(双精度) | 64 | -1.7E308~1.7E308 | 0.0 |
| char(字符) | 16 | 0~255 | ‘\u0000’ |
| boolean(布尔) | - | true或false | false |
double和float的区别是double精度高,有效数字16位,float精度7位引用数据类型
引用数据类型大致包括:类、接口类型、数组、枚举、注解类型、字符串
基本数据类型和引用数据类型的区别
- 存储位置
基本数据类型:在方法中定义的非全局基本数据类型变量的具体内容是存储在栈中的
引用变量类型:只要是引用数据类型变量,其具体内容都是存放在堆中的,而栈中存放的是其在堆中的地址- 传递方式
基本数据类型:在方法中定义的非全局基本数据类型变量,调用方法时作为参数是按数值传递的
引用变量类型:引用数据类型变量,调用方法时作为参数是按引用传递的
22. 值传递和引用传递
值传递是指在方法调用时,传递的参数是按值的拷贝传递
引用传递是指在方法调用时,传递的参数是按引用传递的,其实传递的是引用的地址,也就是变量所对应的内存空间的地址
23. Get和Post请求的区别
- Get请求用来从服务器上获得资源,而Post是用来向服务器提交数据
- Get将表单中的数据按照name=value的形式,添加到action所指向的URL后面,并且两者使用“?”连接,而各个变量之间使用“&”连接;Post是将表单中的数据放在HTTP协议的请求头或消息体中,传递到action所指向URL
- Get传输的数据收到URL长度限制(1024字节);而Post可以传输大量的数据,上传文件通常要使用Post方式
- 使用Get时参数会显示在地址栏上,如果这些数据不是敏感数据,那么可以使用Get;对于敏感数据需要使用Post
- Get请求只能进行URL编码,而Post支持多种编码
- Get只接收ASCLL字符,而Post没有限制
24. 请求转发和重定向
forward是转发,redirect是重定向,是servlet的两种主要跳转方式
区别:
- 地址栏显示
forward是服务器内部的重定向,服务器直接访问目标地址的URL
redirect是服务器根据逻辑,发送一个状态码,告诉浏览器重新去请求那个地址,所以地址栏显示的是新地址
- 数据共享
forward会将request的信息带到被重定向的JSP或Servlet中使用,可以共享数据
redirect不能共享数据- 效率
forward效率高,而redirect效率低
- 本质
forward转发是服务器上的行为,而redirect重定向是客户端的行为
- 请求次数
forward只有一次请求,而redirect有两次请求
转发过程
客户浏览器发送http请求,web服务器接受此请求,调用内部的一个方法在容器内部完成请求处理和转发动作,将目标资源发送给客户。转发的路径必须是同一个web容器下的URL,其不能转向到其它的web路径上去,中间传递的是自己的容器内的request。在客户浏览器路径栏显示的仍然是其第一次访问的路径,也就是说客户感觉不到服务器做了转发。转发行为是浏览器只做了一次访问请求
重定向过程
客户浏览器发送http请求,web服务器接受后发送302状态码响应及对应新的location给客户浏览器,客户浏览器发现是302响应,则自动再发送一个新的http请求,请求URL是新的location地址,服务器根据此请求寻找资源并发送给客户。在这里location可以重定向到任意URL,既然是浏览器重新发出了请求,则没有什么request传递的概念了。在客户浏览器路径栏显示的是其重定向的路径,客户可以观察到地址的变化,重定向行为是浏览器至少两次的访问请求
第一次,客户端request A,服务器响应,并response回来,告诉浏览器应该去B。重定向可以访问自己web应用以外的资源,在重定向过程中,传输的信息会被丢失
25. final关键字
在JAVA中,final关键字可以用来修饰类、方法和变量(包括成员变量和局部变量)
- 修饰类
当用final修饰一个类时,这个类将不能被继承
- 修饰方法
使用final方法的原因有两个
第一,把方法锁定,以防任何继承类修改它的含义
第二,因为效率,只有在明确禁止该方法在子类中被覆盖的情况下才将方法设置为final
注:类的private方法会被隐式的声明为final- 修饰变量
对于一个final变量,如果是基本数据类型的变量,则其数值一但在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象,但是它指向的对象的内容是可变的
26. 强引用、软引用、弱引用和虚引用的区别
- 强引用:最普遍的一种引用方式,如String s = “abc”,变量s就是字符串”abc”的强引用,只要强引用存在,则垃圾回收器就不会回收这个对象
- 软引用:用于描述还有用但非必须的对象,如果内存足够,不回收;如果内存不足,则回收。一般用于实现内存敏感的高速缓存,软引用可以和引用队列ReferenceQueue联合使用,如果软引用的对象被垃圾回收,JVM就会把这个软引用加入到与之关联的引用队列中
- 弱引用:弱引用和软引用大致相同,弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一但发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。短时间内通过弱引用取对应的数据可以取到,当执行第二次垃圾回收时,将返回null。弱引用主要用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的isEnQueued方法返回对象是否被垃圾回收器标记
- 虚引用:与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中
27. String类常用方法
方法名 说明 String(char[] value) 将传入的字符数组构造为字符串 String(char[] value, int offset, int count) 将部分字符数组构造为字符串 char charAt(int index) 获取指定索引位置的字符 char[] toCharArray() 将字符串中的数据以字符数组
1
2
3
4
5
6
7
8
9
void test4()
{
String s1 = "hello world";
char[] value = s1.toCharArray(); // 将字符串转换为字符数组
String s2 = new String(value, 0, 5); // 将字符数组指定位置的字符转换为字符串
System.out.println(s2);
char ch = s2.charAt(2); // 获取字符串指定位置的字符
}字符串比较方法
方法名 说明 boolean equals(String anObject) 区分大小写比较 boolean equalsIgnoreCase(String anotherString) 不区分大小写比较 int compareTo(String anotherString) 进行字符串大小比较,该方法返回一个int数据:大于、小于、等于 int compateToIgnoreCase(String str) 不区分大小写字符串大小比较
1
2
3
4
5
6
7
8
9
10
void test4()
{
String s1 = "hello world";
String s2 = "HELLO WORLD";
System.out.println(s1.equals(s2)); // false
System.out.println(s1.equalsIgnoreCase(s2)); // true
System.out.println(s1.compareTo(s2)); // 32
System.out.println(s1.compareToIgnoreCase(s2)); // 0
}字符串查找
方法名 说明 boolean contains(String s) 判断子字符串是否存在 int indexOf(String str) 从头查找指定字符串的位置 int indexOf(String str, int fromindex) 从指定位置查找指定字符串的位置 int lastIndexOf(String str) 由后向前查找指定字符串的位置 int lastIndexOf(String str, int fromIndex) 从指定位置由后向前查找指定字符串的位置 boolean startsWith(String prefix) 判断是否以指定的字符串开头 boolean startsWith(String prefix, int toffset) 由指定位置判断是否以指定字符串开头 boolean endsWith(String suffix) 判断是否以指定的字符串结尾
1
2
3
4
5
6
7
8
9
10
11
12
13
public void test2()
{
String str = "aaabbbccc";
System.out.println(str.contains("abb")); // true
System.out.println(str.indexOf("aab")); // 1
System.out.println(str.indexOf("bcc", 3)); // 5
System.out.println(str.lastIndexOf("bb")); // 4
System.out.println(str.lastIndexOf("bb", 7)); // 4
System.out.println(str.startsWith("aa")); // true
System.out.println(str.startsWith("bb", 2)); // false
System.out.println(str.endsWith("ccc")); // true
}字符串替换
方法名 说明 String replaceAll(String regex, String replacement) 全部替换 String replaceFirst(String regex, String replacement) 替换首个
1
2
3
4
5
6
7
public void test3()
{
String str = "abccba";
System.out.println(str.replaceAll("a", "c")); // cbccbc,将所有的a替换为c
System.out.println(str.replaceFirst("c", "a")); // abacba,将从左到右第一个遇见的c替换为a
}字符串拆分
方法名 说明 String[] split(String regex) 按照指定的字符串全部拆分 String[] split(String regex, int limit) 按照指定的字符串拆分为指定个数,后面不拆了
1
2
3
4
5
6
7
8
9
public void test4()
{
String str = "abcbabcaabcacbb";
System.out.println(Arrays.toString(str.split("abc"))); // [, b, a, acbb],第一个abc前面没有字符串所以为空
System.out.println(Arrays.toString(str.split("abc", 3))); // [, b, aabcacbb]
String str1 = "hello word this";
System.out.println(Arrays.toString(str1.split(" "))); // [hello, word, this]
}遇到正则表达式需要转义,”\\“
1
2
3
4
5
6
public void test4()
{
String str = "192.168.0.0";
System.out.println(Arrays.toString(str.split("\\."))); // [192, 168, 0, 0]
}字符串截取
方法名 说明 String substring(int beginIndex) 从指定索引截取到结尾 String substring(int beginIndex, int endIndex) 截取指定索引范围中的子字符串
1
2
3
4
5
6
7
public void test5()
{
String str = "hello world";
System.out.println(str.substring(2)); // llo world
System.out.println(str.substring(2, 5)); // llo
}其他方法
方法名 说明 String concat(String str) 字符串的连接 String intern() 字符串入池 boolean isEmpty() 判断字符串是否为空(不是null) int length() 返回字符串的长度 String trim() 去除左右的空格字符 String toUpperCase() 将小写字符转为大写 String toLowerCase() 将大写字符转为小写
1
2
3
4
5
6
7
8
9
10
11
12
13
public void test5()
{
String str = " hello world ";
System.out.println(str.concat("abc")); // hello world abc
System.out.println(str.intern()); // hello world
String str1 = " hello world ";
System.out.println(str == str1); // true
System.out.println(str.isEmpty()); // false
System.out.println(str.length()); // 15
System.out.println(str.trim()); // hello world
System.out.println(str.toUpperCase()); // HELLO WORLD
}校验一个String类型的变量是否为空
通常存在三种情况:
- 是否为null
- 是否为””
- 是否为空字符串,即” “
StringUtils的isBlank()方法可以一次性校验这三种情况,返回值都是true,否则为false
1
2
3
4
5
6
7
8
public void test1()
{
System.out.println(StringUtils.isBlank(" ")); // true
System.out.println(StringUtils.isBlank("")); // true
System.out.println(StringUtils.isBlank(null)); // true
System.out.println(StringUtils.isBlank("a")); // false
}
28. Scanner方法
Scanner类的主要功能是简化文本扫描,这个类最实用的地方表现在获取控制台输入
Scanner类里面有如下几种操作方法:
- 构造方法:public Scanner(InputStream source)
- 判断是否有数据:public boolean hasNext()
- 取出数据:public String next()
- 设置分隔符:public Scanner useDelimiter(String pattern)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 public static void main(String[] args)
{
Scanner scan = new Scanner(System.in);
System.out.println("请输入年龄:");
if(scan.hasNextInt())
{
int age = scan.nextInt();
System.out.println("年龄:" + age);
}
else
{
System.out.println("输入格式错误");
}
scan.close();
}使用正则表达式验证判断
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 public static void main(String[] args)
{
Scanner scan = new Scanner(System.in);
System.out.print("请输入您的生日:");
if(scan.hasNext("\\d{4}-\\d{2}-\\d{2}")) // 正则表达式判断输入格式
{
String str = scan.next();
System.out.println("生日为:" + str);
}
else
{
System.out.println("输入格式错误");
}
scan.close();
}读取文件内容
1
2
3
4
5
6
7
8
9
10 public static void main(String[] args) throws FileNotFoundException
{
Scanner scan = new Scanner(new File("D:"+File.separator+"test.txt"));;
scan.useDelimiter("\n"); // 设置读取分隔符
while(scan.hasNext())
{
System.out.println(scan.next());
}
scan.close();
}
29. 序列化
所谓的对象序列化指的是将内存中保存的对象以二进制数据流的形式进行处理,可以实现对象的保存或者是网络传输
然而并不是所有的对象都可以被序列化,在JAVA里面有一个强制性的要求:如果要序列化对象,则该对象所在的类一定要实现java.io.Serializable接口,作为序列化的标记,这个接口没有任何方法,它描述的是类的一种能力
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 class Person implements Serializable
{
private String name;
private int age;
private Person(String name,int age)
{
this.name = name;
this.age = age;
}
public String toString() {
return "姓名:"+this.name + "年龄:"+ this.age;
}
}此时Person类产生的每一个对象都可以实现二进制的数据传输,属于可以被序列化的程序类
30. 定时任务Spring Task
如果使用的是Spring或Spring Boot框架,可以直接使用Spring Framework自带的定时任务
以Spring Boot为例,实现定时任务只需两步:
- 开启定时任务
- 添加定时任务
开启定时任务
开启定时任务只需要在Spring Boot的启动类上声明@EnableScheduling注解即可,实现代码如下:
1
2
3
4
// 开启定时任务
public class DemoApplication {
}添加定时任务
定时任务的添加只需要使用@Scheduled注解标注即可,如果有多个定时任务可以创建多个@Scheduled注解标注的方法,示例代码如下:
1
2
3
4
5
6
7
8
9
10 import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
// 把此类托管给 Spring,不能省略
public class TaskUtils {
// 添加定时任务
// cron 表达式,每周五 23:59:59 执行
public void doTask(){
System.out.println("我是定时任务~");
}
}注意:定时任务是自动触发的无需手动干预,也就是说 Spring Boot 启动后会自动加载并执行定时任务
Cron表达式
Spring Task的实现需要使用cron表达式来声明执行的频率和规则,cron表达式由6位或7位组成(最后一位可省略),每位之间以空格分隔,每位从左到右代表含义如下:
*其中和?都表示匹配所有的时间 **
cron 表达式在线生成地址:https://cron.qqe2.com/
31. Lambda表达式
Lambda表达式详解
Lambda表达式是JDK8的一个新特性,可以取代大部分的匿名内部类,写出更优雅的JAVA代码,尤其在集合的遍历和其他集合操作中,可以极大地优化代码结构
JDK也提供了大量的内置函数式接口供我们使用,使得Lambda表达式的运用更加方便、高效
@FunctionalInterface,修饰函数式接口的,要求接口中的抽象方法只有一个。这个注解往往会和Lambda表达式一起出现Lambda基础语法
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 // 多参无返回值
interface NoRetMultiParam{
void method(int a, int b);
}
// 无参无返回值
interface NoRetNoParam{
void method();
}
// 一个参数无返回值
interface NoRetOneParam{
void method(int a);
}
// 多个参数有返回值
interface RetMultiParam{
int method(int a, int b);
}
// 无参有返回值
interface RetNoParam{
int method();
}
// 一个参数有返回值
interface RetOneParam{
int method(int a);
}语法形式为()->{},其中()用来描述参数列表,{}用来描述方法体,->为Lambda运算符,读作(goes to)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 public static void main(String[] args)
{
NoRetNoParam n1 = () -> System.out.println("NoRetNoParam");
NoRetOneParam n2 = (a) -> System.out.println("a: " + a);
NoRetMultiParam n3 = (a, b) -> System.out.println("a: " + a + ", b: " + b);
RetNoParam r1 = () -> 1;
RetOneParam r2 = (a) -> a + 1;
RetMultiParam r3 = (a, b) -> a + b;
n1.method();
n2.method(1);
n3.method(1, 2);
System.out.println(r1.method());
System.out.println(r2.method(1));
System.out.println(r3.method(1, 2));
}Lambda表达式常用示例
有时候我们不是必须要自己重写某个匿名内部类的方法,我们可以利用lambda表达式的接口快速指向一个已经被实现的方法
方法归属者::方法名,静态方法的归属者为类名,普通方法归属者为对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 public static void main(String[] args)
{
RetOneParam r1 = a -> fun1(a);
RetOneParam r2 = test1::fun1;
test1 t1 = new test1();
RetOneParam r3 = t1::fun2;
System.out.println(r1.method(1));
System.out.println(r2.method(1));
System.out.println(r3.method(1));
}
/**
*参数数量和类型要与接口中定义的一致
*返回值类型要与接口中定义的一致
*/
public static int fun1(int a)
{
return a * 2;
}
public int fun2(int a)
{
return a + 2;
}构造方法的引用
一般我们需要声明接口,该接口作为对象的生成器,通过类名::new的方式来实例化对象,然后调用方法返回对象
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 public class Item
{
int id;
String name;
double price;
public Item(){}
public Item(int i, String s, double v){}
}
public class test2
{
public static void main(String[] args)
{
ItemCreatorBlankConstruct c1 = () -> new Item();
Item item1 = c1.getItem();
ItemCreatorBlankConstruct c2 = Item::new;
Item item2 = c2.getItem();
ItemCreatorParamConstruct c3 = Item::new;
Item item3 = c3.getItem(1, "鼠标", 140);
}
}
interface ItemCreatorBlankConstruct{
Item getItem();
}
interface ItemCreatorParamConstruct{
Item getItem(int id, String name, double price);
}Lambda表达式创建线程
以往创建Thread对象,通过匿名内部类重写run()方法,提到匿名内部类就可以使用Lambda表达式来简化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 public static void main(String[] args)
{
Thread t1 = new Thread(new Runnable(){
public void run(){
for(int i = 0; i < 10; i++)
{
System.out.println(i);
}
}
});
t1.start();
Thread t2 = new Thread(()->{
for(int i = 0; i < 10; i++)
{
System.out.println(i);
}
});
t2.start();
}Lambda表达式遍历集合
1
2
3
4
5
6
7
8
9 public static void main(String[] args)
{
ArrayList<Integer> list = new ArrayList<>();
Collections.addAll(list, 1, 2, 3, 4, 5);
list.forEach(System.out::println);
list.forEach(element -> {
if(element % 2 == 0) System.out.println(element);
});
}
32. BigDecimal
JAVA中提供了大数字(超过16位有效位)的操作类,即java.math.BinInteger类和java.math.BigDecimal类,用于高精度计算
其中BigInteger类是针对大整数的处理类,而BigDecimal类则是针对大小数的处理类
BigDecimal类的实现用到了BigInteger类,不同的是BigDecimal加入了小数的概念float和Double只能用来做科学计算或者是工程计算,在商业计算中,对数字精度要求较高,必须使用BigInteger类和BigDecimal类,它支持任何精度的定点数,可以用它来精确计算货币值
BigDecimal类创建的是对象,不能使用传统的+、-、×、/等算术运算符直接对其进行数学运算,而必须调用其对应的方法,方法的参数也必须是BigDecimal类型的对象构造BigDecimal对象常用方法
1
2
3 BigDecimal BigDecimal(double d); // 不允许使用
BigDecimal BigDecimal(String s); // 常用,推荐使用
static BigDecimal valueOf(double d); // 常用,推荐使用注意:
double参数的构造方法,不允许使用,因为它不能精确的得到相应的值,值会变大
String参数的构造方法是完全可预知的,写入new BigDecimal(“0.1”)将创建一个BigDecimal,它正好等于预期的0.1,因此通常优先使用String构造方法
静态方法valueOf(double d)内部实现,仍是将double类型转为String类型;这通常是将double(或float)转化为BigDecimal的首选方法常用方法
方法名 说明 add(BigDecimal) BigDecimal对象中的值相加,然后返回这个对象 subtract(BigDecimal) BigDecimal对象中的值相减,然后返回这个对象 multiply(BigDecimal) BigDecimal对象中的值相乘,然后返回这个对象 divide(BigDecimal) BigDecimal对象中的值相除,然后返回这个对象 toString() 将BigDecimal对象的数值转换成字符串 doubleValue() 将BigDecimal对象的值以双精度返回 floatValue() 将BigDecimal对象的值以单精度返回 longValue() 将BigDecimal对象的值以长整数返回 intValue() 将BigDecimal对象的值以整数返回 四舍五入
方法名 说明 BigDecimal.setScale() 用于格式化小数点 setScale(1) 表示保留一位小数,默认用四舍五入方式 setScale(1, BigDecimal.ROUND_DOWN) 直接删除多余的小数位,如2.35变成2.3 setScale(1, BigDecimal.ROUND_UP) 进位处理,2.32变成2.4 setScale(1, BigDecimal.ROUND_HALF_UP) 四舍五入,2.35变成2.4 setScale(1, BigDecimal.ROUND_HALF_DOWN) 四舍五入,2.35变成2.3,如是小数位是5则向下舍 setScale(1, BigDecimal.ROUND_CEILING) 接近正无穷大的舍入 setScale(1, BigDecimal.ROUND_FLOOR) 接近负无穷大的舍入,数字>0和ROUND_UP作用一样,数字<0和ROUND_DOWN作用一样 setScale(1, BigDecimal.ROUND_HALF_EVEN) 向最接近的数字舍入,如果与两个相邻数字的距离相等,则向相邻的偶数舍入 BigDecimal数据的比较
1
2
3
4
5
6
7
public void test1()
{
BigDecimal b1 = new BigDecimal("111.32");
BigDecimal b2 = new BigDecimal("112");
System.out.println(b1.compareTo(b2));
}当两个对象的值相等时返回0,当b1的值小于b2时返回-1,当b1的值大于b2时返回1