语言特性
面向对象编程三大特性
封装:封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法。
继承:继承就是子类继承父类的特征和行为,使用继承我们能够非常方便地复用以前的代码。
多态:多态就是同一个接口,使用不同的实例而执行不同操作
Java和C++的区别?
- 都是面向对象的语言,都支持封装、继承、多态
- java不提供指针访问内存,程序内存更安全
- java的类是单继承,C++支持多继承。但是java接口可以多继承
- java有自动内存管理机制,不需要程序员手动释放无用内存
JDK和JRE的区别
- JDK:Java Development Kit(java开发工具包)
- JRE:Java Runtime Enviroment(Java运行时环境)
JDK包含JRE。JRE是运行必要的环境,可单独运行。
如果你需要运行java程序,只需安装JRE就可以了。如果你需要编写java程序,需要安装JDK。
JDK8 的新特性
- 用元空间替代了永久代
- 引入了 Lambda 表达式
- 引入了日期类(LocalDate、LocalTime、LocalDateTime)
- 引入了接口的默认方法和静态方法
- 修改了 HashMap 和 ConcurrentHashMap 的实现。
**(然后等着被问)
浅拷贝和深拷贝
浅拷贝:只复制指针,新旧对象共享同一块内存。
深拷贝:创建一个一模一样的对象,新旧对象不共享内存。修改新对象不会改到原对象。
注解
注解是一种标记,使类或接口附加额外信息,帮助编译器和 JVM 完成一些特定功能,例如 @Override
标识一个方法是重写方法。
元注解是自定义注解的注解,例如:
@Target
:约束作用位置
@Rentention
:约束生命周期
@Documented
:表明这个注解应该被 javadoc 记录。
泛型
泛型的本质即 “参数化类型”,就是可以把类型像方法的参数那样传递。
- 类型安全,放置什么类型,取出来就是什么类型,不存在 ClassCastException 类型转换异常。
- 提升可读性,编码阶段就显式知道泛型集合、泛型方法等处理的对象类型。
- 代码重用,合并了同类型的处理代码。
泛型的实现原理
Java 的泛型是伪泛型。
实现原理是类型擦除。Java的泛型只存在于编译期,一旦编译成字节码,泛型将被擦除。泛型的作用在于在编译阶段保证我们使用了正确的类型,并且由编译器帮我们加入转型动作,使得转型是不需要关心且安全的。
反射
在运行状态中,对于任意一个类都能知道它的所有属性和方法,对于任意一个对象都能调用它的任意方法和属性,这种动态获取信息及调用对象方法的功能称为反射。
反射的缺点是破坏了封装性以及泛型约束。
Class 类
在程序运行期间,Java 运行时系统为所有对象维护一个运行时类型标识,这个信息会跟踪每个对象所属的类,虚拟机利用运行时类型信息选择要执行的正确方法,保存这些信息的类就是 Class,这是一个泛型类。
获取 Class 对象:① 类名.class
。② 对象的 getClass
方法。③ Class.forName(类的全限定名)
。
序列化与反序列化
- 序列化其实就是将对象转化成可传输的字节序列格式,以便于存储和传输。
- 反序列化就是将字节序列格式转换成对象的过程。
Java是值传递还是引用传递
JVM 内存有划分为栈和堆,局部变量和方法参数是在栈上分配的,基本类型和引用类型都占 4 个字节,当然 long 和 double 占 8 个字节。
而对象所占的空间是在堆中开辟的,引用类型的变量存储对象在堆中地址来访问对象,所以传递的时候可以理解为把变量存储的地址给传递过去,因此引用类型也是值传递。
Integer缓存池
因为根据实践发现大部分的数据操作都集中在值比较小的范围,因此 Integer 搞了个缓存池,默认范围是 -128 到 127 。可以通过 JVM 参数修改缓存的最大值,最小值不能改。
Integer a = 127;
Integer a1 = 127;
System.out.println(a == a1); //true
Integer b = 128;
Integer b1 = 128;
System.out.println(b == b1); //false
异常
Exception 和 Error 都是继承了Throwable类,他们的区别如下:
- Exception 是程序正常运行过程中可以预料到的意外情况,应该被开发者捕获并且进行相应的处理。
- Error 是指在正常情况下不太可能出现的情况,绝大部分的 Error 都会导致程序挂掉。
最常见的RuntimeException:
- NullPointerException 空指针异常。(调用了未经初始化 或者 不存在的对象)
- NumberFormatException 字符串转换为数字异常。(字符串中包含非数字型字符)
- IndexOutOfBoundsException 数组角标越界异常。
- IllegalArgumentException 方法传递参数错误
- ClassCastException 数据类型转换异常
数据类型
基本数据类型
- 1字节 = 8位二进制
//四种整数类型:
byte b = 127; //1字节
short s 32767; //2字节
int i = 0; //4字节
long l = 0L; //8字节,范围值比int更大
//两种浮点类型:
float f = 0.00F; //4字节
double d = 0.00; //8字节
//一种字符类型:
char c = 'a'; //2字节
//一种布尔类型:
boolean bl = true; //1字节
自动装箱/拆箱
每个基本数据类型都对应一个包装类,除了 int 和 char 对应 Integer 和 Character 外,其余基本数据类型的包装类都是首字母大写即可。
自动装箱: 将基本数据类型包装为一个包装类对象,例如向一个泛型为 Integer 的集合添加 int 元素。
自动拆箱: 将一个包装类对象转换为一个基本数据类型,例如将一个包装类对象赋值给一个基本数据类型的变量。
比较两个包装类数值要用 equals
,而不能用 ==
。
包装类的作用
我们知道Java是一个面向对象的编程语言,基本类型并不具有对象的性质,为了让基本类型也具有对象的特征,就出现了包装类型。例如我们在使用集合类型时就一定要使用包装类型。
它相当于将基本类型“包装”起来,使得它具有对象的性质,并且为其添加了属性和方法,丰富了基本类型的操作。
包装类型与基本类型的区别
- 声明方式不同:包装类型需要用new关键字创建。
- 存储方式不同:基本类型直接将变量存储在栈中;而包装类型将对象放在堆中,通过引用来使用。
- 初始值不同:基本类型的初始值如int为0,boolean为false;而包装类型的初始值为null
String str="i"与 String str=new String("i")一样吗?
不一样。内存的分配方式不一样。
String str="i"
:JVM会将其分配到常量池中,如果常量池有i
,则返回i
的地址。没有就创建i
String str=new String("i")
:被分配到堆内存中新开辟一块空间。
String s1 = new String("abc");这句话创建了几个字符串对象?
将创建 1 或 2 个字符串。如果池中已存在字符串文字“abc”,则池中只会创建一个字符串“s1”。如果池中没有字符串文字“abc”,那么它将首先在池中创建,然后在堆空间中创建,因此将创建总共 2 个字符串对象。
StringBuffer、StringBuilder的区别
内存:
String是不可变的对象;(被final修饰)
StringBuffer、StringBuilder可以在原有对象的基础上进行操作;(可变)
线程安全:
StringBuffer线程安全,有同步锁
StringBuilder线程不安全- 效率:StringBuilder > StringBuffer > String
使用场景:
String:操作少量数据,字符串不变时
StringBuilder:单线程操作大量数据(方法内部,用完回收)
StringBuffer:多线程操作大量数据(主要用在全局变量中)
StringBuffer和StringBuilder都继承自抽象类AbstractStringBuilder。抽象类AbstractStringBuilder内部都提供了一个自动扩容机制,当发现长度不够的时候(初始默认长度是16),会自动进行扩容工作,扩展为原数组长度的2倍加2。
ps:超过16个字符,尽可能指定容量,不指定会显著降低性能。
String不可变性
String 类和其存储数据的成员变量 value 字节数组都是 final 修饰的。对一个 String 对象的任何修改实际上都是创建一个新 String 对象,再引用该对象。只是修改 String 变量引用的对象,没有修改原 String 对象的内容。
好处:
① 节省常量池的内存空间。(执行String s = "abc"
时,JVM会先到常量池找“abc”对象)
② 允许String对象缓存hashcode(不需要每次重新计算hashcode)
③ 安全性(String有许多地方当做参数,如果被人改变String引用指向的对象,会造成安全漏洞)
基本操作
== 和 equals 的区别是什么?
功能不同
==
的作用:- 对于基本类型:比较的是值是否相同
- 对于引用类型:比较的是引用是否相同
equals
作用:- equals本质就是==,只不过String和Integer等重写了equals方法,把它变成了值比较。(内容是否相等)
定义不同
==
是一个运算符号,equals()
是java的一个方法
字符串反转
- 利用
StringBuilder
自带的reverse()
方法(要new一个StringBuilder对象,将代反转的字符串作为参数传入) 调用toCharArray()将字符串转换为char[]数组,倒序遍历数组
String s = "hello java"; //调用toCharArray()将字符串转换为char[]数组,倒序遍历数组 char[] array = s.toCharArray(); StringBuilder s1 = new StringBuilder(); for (int i = array.length - 1; i >= 0; i--) { s1.append(array[i]); // 倒序遍历的字符添加进StringBuilder对象中 } System.out.println(s1); //输出反转数组
两个对象的 hashCode()相同,则 equals()也一定为 true吗?
不对,两个对象的 hashCode()相同,equals()不一定 true。("通话" 、 "重地")
而且equals方法可以被重写。
String 类的常用方法都有那些?
删:
方法 | 作用 |
---|---|
trim() |
删除字符串的头尾空白符 |
subString() |
1、从指定位置截取到字符串结尾 2、截取指定范围的内容 |
String s = "HiHellojava"; //电脑从0开始计数
String s1 = s.substring(2, 7); //截取Hello。从2的H开始截,结果不包含第7个位置的内容。
改:
方法 | 作用 |
---|---|
valueOf(基本数据型态) |
将基本数据型态转换成String(char[]也行,int[]不行) |
toUpperCase() |
字符转换为大写后的字符串。 |
toUpperCase() |
字符转换为小写后的字符串。 |
查:
方法 | 作用 |
---|---|
length() |
返回字符串的长度 |
indexOf() |
查找字符串开始出现的位置,返回下标,不存在返回-1。 |
charAt() |
返回指定索引处的字符。索引范围为从 0 ~ length()-1 。 |
startsWith() |
测试此字符串是否以指定的前缀开始。返回一个布尔值 |
endsWith() |
测试此字符串是否以指定的后缀结束。返回一个布尔值 |
为啥重写equals要重写hashCode()?
例如在 HashMap 中,如果 key 重复的话,就会覆盖之前的值。
如果传入的key是一个我们创建的对象,重写hashcode方法能使他们的hashcode相同。重写equals方法能使他们用 equals() 比较能够判断相同。
但是HashMap判断覆盖的方法是先比较hashcode值再比较equals,如果我们不重写hashCode,就不能成功的覆盖key。
try...catch...finally情况
在 try 中的 return 真正返回之前会执行 finally 中的语句。如果 finally 中有 return,就不会执行 try 中的 return 了。
public static void main(String[] args) {
System.out.println("main..." + getValue()); // 返回0
}
public static int getValue() {
int i = 0;
try {
System.out.println("try...");
return i;
} finally {
System.out.println("finally...");
i++;
}
}
上面例子返回的 i 值为 0,因为 try 中有 return ,会先将 try 中的变量放到本地变量表,finally中的操作不会影响 i 的值。(如果finally中也 return i
,那就不会执行 try 中的 return了)
public static void main(String[] args) {
System.out.println("main..." + getValue());
}
public static int getValue() {
int i = 0;
try {
int result = 5 / 0;
} catch (Exception e) {
return i++;
} finally {
++i;
}
return i;
}
try 出现异常,执行 catch,catch 有 return,先执行 finally 的内容,但是 finally 里的操作不会影响 i 的值。
i 还是为 0,最后执行 return i++
,先返回 i ,再执行加一操作。所以方法返回的是 0。
public static void main(String[] args) {
System.out.println("main..." + getValue()); // 返回0
}
public static int getValue() {
int i = 0;
try {
int result = 5 / 0;
} catch (Exception e) {
return i++;
} finally {
return ++i;
}
}
在 catch 语句中,在执行 return 语句时,要返回的结果已经准备好了(i=1),就在此时,程序转到 finally 执行了,此时 return ++i
把原来存放到本地变量表的 i 加一并返回,返回的结果为 2。
建议:① 不要在catch和finally块中有return语句;② 建议在finally中只进行资源的清理操作;
面向对象
重载和重写
重载指方法名称相同,但参数类型个数不同,是行为水平方向不同实现。对编译器来说,方法名称和参数列表组成了一个唯一键,称为方法签名,JVM 通过方法签名决定调用哪种重载方法。不管继承关系如何复杂,重载在编译时可以根据规则知道调用哪种目标方法,因此属于静态绑定。
JVM 在重载方法中选择合适方法的顺序:① 精确匹配。② 基本数据类型自动转换成更大表示范围。③ 自动拆箱与装箱。④ 子类向上转型。⑤ 可变参数。
重写指子类实现接口或继承父类时,保持方法签名完全相同,实现不同方法体,是行为垂直方向不同实现。
元空间有一个方法表保存方法信息,如果子类重写了父类的方法,则方法表中的方法引用会指向子类实现。父类引用执行子类方法时无法调用子类存在而父类不存在的方法。
重写方法访问权限不能变小,返回类型和抛出的异常类型不能变大,必须加 @Override
。
Object类
equals:检测对象是否相等,默认使用 ==
比较对象引用。可以重写 equals 方法自定义比较规则。x.equals(null)
返回 false。
hashCode:根据对象的地址或者内容算出来的int类型的数值,值可能相同。为了在集合中正确使用,一般需要同时重写 equals 和 hashCode,要求 equals 相同 hashCode 必须相同。支持此方法是为了提高哈希表的性能。
toString:打印对象时默认的方法,如果没有重写打印的是表示对象值的一个字符串。
clone:naitive方法,用于创建并返回 当前对象的一份拷贝。
finalize:方法将在垃圾回收器清除对象之前调用,但具有不定性
getClass:返回包含对象信息的类对象。
wait:让线程处于等待状态,锁会释放
notify / notifyAll :唤醒在等待队列的某个 / 所有线程
final 修饰符
- 修饰类:表示该类不能被继承 (谨慎选择)
- 修饰方法:表示方法不能被重写
- 修饰变量:表示变量只能一次赋值以后值不能被修改(常量)
引用变量被final修饰之后,虽然不能再指向其他对象(指向内存地址不会变),但是该内存地址中保存的对象信息, 是可以进行修改的.
接口和抽象类
语法维度 | 抽象类 | 接口 |
---|---|---|
成员变量 | 无特殊要求 | 默认 public static final 常量 |
构造方法 | 有构造方法,不能实例化 | 没有构造方法,不能实例化 |
方法 | 抽象类可以没有抽象方法,但有抽象方法一定是抽象类。 | 默认 public abstract,JDK8 支持默认/静态方法,JDK9 支持私有方法。 |
继承 | 单继承 | 多继承 |
抽象类不能用final修饰,因为定义抽象类就是为了让其他类继承的。
访问权限控制符
访问权限控制符 | 本类 | 包内 | 包外子类(继承关系) | 任何地方 |
---|---|---|---|---|
public | √ | √ | √ | √ |
protected | √ | √ | √ | × |
无 | √ | √ | × | × |
private | √ | × | × | × |
子类初始化顺序
- 父类静态代码块和静态变量。
- 子类静态代码块和静态变量。
- 父类普通代码块和普通变量。
- 父类构造方法。
- 子类普通代码块和普通变量。
- 子类构造方法。
先静态后普通、先父后子(普通成员变量、普通代码块、构造函数按照代码先后顺序执行)
线程
多线程的优缺点
优点
1、加快程序的运行速度,使程序的响应速度更快。
2、可以把占据长时间的程序中的任务放到后台去处理,同时执行其他操作,提高效率
3、可以随时停止任务
4、可以分别设置各个任务的优先级
缺点
1、线程切换会消耗系统内存
2、同一进程中某一个多线程结束时,其他线程也要结束
3、由于多个线程之间存在共享数据,因此容易出现线程死锁的情况
4、对线程的管理需要额外的 CPU 开销
线程是越多越好吗?
不是,线程多了可以提高程序的并行执行速度,但不是越多越好。
线程越多,就意味着更多的内存资源被占用。而且,一个CPU不是同时执行多个线程的,而是轮流执行的,线程太多,CPU必须不断的在各个线程间快速更换,线程间的切换消耗了许多时间,导致CPU利用率反而下降了。
线程的生命周期
new:新建状态,线程被创建且未启动,此时还未调用 start
方法。
runnable:Java 将操作系统中的就绪和运行两种状态统称为 RUNNABLE,此时线程有可能在等待时间片,也有可能在执行。
blocked:阻塞状态,可能由于锁被其他线程占用、调用了 sleep
或 join
方法、执行了 wait
方法等。
waiting:等待状态,该状态线程不会被分配 CPU 时间片,需要其他线程通知或中断。可能由于调用了无参的 wait
和 join
方法。
time_waiting:限期等待状态,可以在指定时间内自行返回。可能由于调用了带参的 wait
和 join
方法。
terminated:终止状态,表示当前线程已执行完毕或异常退出。
线程的创建方式
① 继承 Thread 类并重写 run 方法。实现简单,但不符合里氏替换原则,不可以继承其他类。
② 实现 Runnable 接口并重写 run 方法。避免了单继承局限性,编程更加灵活,实现解耦。
③实现 Callable 接口并重写 call 方法。可以获取线程执行结果的返回值,并且可以抛出异常。
关于线程的几种方法
① sleep
方法会导致当前线程进入休眠状态,与 wait
不同的是该方法不会释放锁资源,进入的是 TIMED-WAITING 状态。
② yiled
方法使当前线程让出 CPU 时间片给优先级相同或更高的线程,回到 RUNNABLE 状态,与其他线程一起重新竞争CPU时间片。
③ join
方法用于等待其他线程运行终止,如果当前线程调用了另一个线程的 join 方法,则当前线程进入阻塞状态,当另一个线程结束时当前线程才能从阻塞状态转为就绪态,等待获取CPU时间片。底层使用的是wait,也会释放锁。
线程通信的方式
volatile 告知程序任何对变量的读需要从主内存中获取,写必须同步刷新回主内存,保证所有线程对变量访问的可见性。
synchronized 确保多个线程在同一时刻只能有一个处于方法或同步块中,保证线程对变量访问的原子性、可见性和有序性。
等待通知机制指一个线程 A 调用了对象的 wait
方法进入等待状态,另一线程 B 调用了对象的 notify/notifyAll
方法,线程 A 收到通知后结束阻塞并执行后序操作。对象上的 wait
和 notify/notifyAll
如同开关信号,完成等待方和通知方的交互。
管道 IO 流用于线程间数据传输,媒介为内存。PipedOutputStream 和 PipedWriter 是输出流,相当于生产者,PipedInputStream 和 PipedReader 是输入流,相当于消费者。管道流使用一个默认大小为 1KB 的循环缓冲数组。输入流从缓冲数组读数据,输出流往缓冲数组中写数据。当数组已满时,输出流所在线程阻塞;当数组首次为空时,输入流所在线程阻塞。
ThreadLocal 是线程共享变量,但它可以为每个线程创建单独的副本,副本值是线程私有的,互相之间不影响。