赞
踩
1、JVM、JDK和JRE
JVM:是运行Java字节码的虚拟机,字节码和不同系统的虚拟机是实现Java语言跨平台特性的关键所在。我们常用的是HotSpotVM仅仅是JVM的一种实现而已,只要符合JVM规范,各种机构组织都可以构建自己的JVM。
JDK:它拥有JRE拥有的一切,还有编译器(javac)和工具(javadoc、jdb),能够创建和编译程序。
JRE:是Java行时环境,包括:JVM,Java类库、Java命令;但它不能构建新的程序。
2、字节码文件
字节码:是JVM可以理解的代码,不面向任何特定的处理程序,只面向JVM。字节码装换为机器码,需要通过解释器,JVM类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这样的执行效率较低,而且有很多方法是需要多次被调用执行的,所以就引进了JIT(just-in-time-compilation)编译器。JIT属于运行时编译,当JIT编译器第一次编译后,会将字节码对应的机器码保存下来,下一次可以直接使用。所以说Java是解释和编译共存的语言。
注:HotSpot采用惰性评估的做法,根据二八定律,占用大量系统资源的是小部分热点代码,JVM会根据代码的执行情况,执行次数做出一定优化,执行的次数越多,速度也就会越快;JDK9引入了一种新的编译模式AOT(ahead-of-time-compilation),直接将字节码编译成机器码,省去了JIT预热等占用的资源,但是AOT的编译质量是比不上JIT的。
3、Java是解释和编译共存的语言?
编译型语言通过编译器将源代码一次性翻译成可以直接在平台执行的字节码文件,执行效率高,开发效率低。(C、C++、Go、Rust)
解释型语言通过解释器将源代码一句句解释为机器码后执行。执行效率低,开发效率高。(Java、Python、PHP)
即时编译技术:现将源代码编译成字节码,在执行时,将字节码直译为机器码执行
Java需要javac先编译为字节码,再通过解释器执行解释为机器码,所以Java是解释和编译共存的语言
4、Open JDK和Oracle JDK
字符相关:
public static void testArgs01(String arg1,String... args) {
}
public static void testArgs01(String... args) {
}
关键词:是被赋予特殊含义的标识符,都是小写
标识符:给类、方法、变量等起的名字就叫做标识符
标识符的命名规则:
标识符的命名规范:
在Java中数据被称为字面量,使用变量可以反复利用内存
变量其实就是内存当中存储数据的最基本的单元。
变量三要素:变量名、数据类型、值
变量使用”=“赋值,在运行时,等号右边先执行,再赋值给左边
变量可以重复赋值,但不能重复声明,可以同时声明多个变量
变量的作用域:大括号之内,Java中访问有个就近原则
(b=a++),先赋值,再自增。
短路与&&和逻辑&
使用扩展赋值运算符的时候,永远都不会改变运算结果类型。
+ 运算符的作用:求和、字符串拼接。当 + 运算符任意一边是字符串类型,+会进行字符串拼接操作。当一个表达式当中有多个加号的时候
遵循“自左向右”的顺序依次执行。(除非额外添加了小括号,小括号的优先级高)
IF语句:
对于if语句来说,在任何情况下只能有1个分支执行;
带有else分支的,一定可以保证会有一个分支执行,如果没有else分支,可能一个分支也没有执行;
当分支当中java语句只有1条,那么大括号{}可以省略
switch语句:
switch (i) {
case 1: case 2: case 3:
System.out.println(111);
}
while语句和do…while语句:while语句可以一开始就不执行,do…while语句至少执行一次
continue、break和return的区别
一个方法就是一个“功能单元”。方法提高了代码复用性。方法体中的代码都必须遵循自上而下的顺序依次逐行执行;除过main方法是由JVM调用的,其他方法都需要手动调用。
返回值是指我们获取到的某个方法体执行后的结果,可以return具体的值,没有指定接收返回值,可以直接return;
静态方法为什么不能调用非静态成员?
静态方法是属于类的,会在类加载时候被执行,而非静态成员属于实例对象的,只有在对象被实例化之后,才可以访问。
静态方法和实例方法有何不同?
类名.方法名
或对象.方法名
,当在一个方法中调用本类中的另一个方法时,类名.
可以省略。实例方法只能使用对象.方法名
。注:静态方法不属于某个对象,而属于这个类方法重载和方法重写的区别?
private、final、static
,子类就不能重写该方法。但被static
修饰的方法可以被子类再次声明。hashCode()的作用是获取哈希码,哈希码的作用是确定该对象在哈希表中的索引位置。
hashCode()定义在Object类中,是本地方法,是使用C++实现的,通常是将对象的内存地址转换为整数后返回;
为什么定义hashCode()这个方法?
答:以hashSet为例,当我们把对象要加入hashSet中时,会先计算该对象的hash值来确定该对象加入容器的位置。同时也会与其它已经加入容器的对象的hash值做对比。
如果没有相同的hash值,就会认为这个对象没有重复出现;
如果出现了相同的hash值的对象,会再调用equals()方法来检查hash值相同的两个对象是否真的相同。如果还是相同,就会判断为相同对象,不允许其再加入容器之中。如果不同,就会重新散列到其他位置。这样就减少了使用equals()的次数,提高了执行速度。
equals()和hashCode()都是用来比较对象是否相等的,JDK为什么同时提供这两种方法?
答:如果仅提供equals()方法;以hashSet为例,插入容器时,需要和已插入对象挨个equals进行比较,这样效率太低。使用hash值可以快速确定索引位置;
如果仅提供hashCode()方法;hash值相等的两个对象不一定真的相等,因为hashCode()使用的hash算法是存在多个对象传回相同哈希值的情况。(不同的对象得到相同的哈希值这样的现象称作哈希碰撞)
hash值相同,对象不一定相等;hash值不相同,对象一定不相等;hash值相同别切equals为true,两个对象相等。
重写equals()方法时,为什么必须也要重写hashCode()方法?
答:要判断两个相等的对象必须hash值相同,并且equals()方法返回true。只重写equals()方法,可能会导致相同的两个对象hash值不相等。
总结:equals()方法判断两个对象是相等的,那这两个对象的哈希值也必须相等;两个对象只是hash值相等,因为存在哈希碰撞,他们不一定相等。
数据类型用来声明变量,程序在运行过程中根据不同的数据类型分配不同大小的空间。
基本数据类型:
小容量可以直接赋值给大容量,称为自动类型转换;大容量不能直接赋值给小容量,需要使用强制类型转换进行强制转换,存在精度损失。
当整数型字面量没有超出byte的取值范围-128~127,那么这个整数型字面量可以直接赋值给byte类型的变量。(写代码方便)
注意:如果用在银行方面或者财务方面,java中提供了一种精度更高的类型:java.math.BigDecimal
任意一个浮点型都比整数型空间大。 float容量 > long容量
浮点型数据默认被当做double来处理 。如果想让这个浮点型字面量被当做float类型来处理,那么请在字面量后面添加F/f。
整数型的数据默认被当做int类型处理。如果希望该‘整数型字面量’被当做long类型来处理,需要在‘字面量’后面加L/l。
多种数据类型混合运算,各自先转换成容量最大的那一种再做运算;
NULL
,基本类型有其默认值不为NULL
Integer i =1;//自动装箱,1是int类型,直接赋值给Integer类型,基本类型装箱为包装类
Integer integer = Integer.valueOf(1);
System.out.println(integer.equals(i));//true
int n = i;//自动拆箱,i是包装类,直接赋值给int类型,包装类拆箱为基本类型
int nn = i.intValue();
System.out.println(n==nn);//true
注:频繁的拆装箱,会严重影响系统性能。应该尽量避免不必要的拆装箱。
语法形式:成员变量是属于类的,而局部变量是属于方法或某个代码块的;成员变量是可以被访问修饰符
和static
修饰的,局部变量不能;但都可以被final
修饰
**存储方式:**如果成员变量是用static
修饰的,就会在类加载时开辟存储空间。而如果没有static
修饰,那就是存储在栈内存中。
**生存时间:**成员变量是伴随着对象的创建和销毁,局部变量是伴随方法的调用创建和销毁。
**默认值:**成员变量如果没有被赋初始值,会根据类型赋默认值;局部变量则不会默认赋值。
**构造方法的作用:**完成类对象的初始化工作
**构造方法的特点:**构造方法名和类名相同、没有返回值但也无需用void
声明、创建对象时自动调用、构造方法可以被重载,但不能重写。
如果一个类没有声明构造方法,该程序能正确执行吗?
答:如果一个类没有声明构造方法,程序也可以执行。因为即使没有声明构造方法,但也会有默认的无参构造方法。如果我们自己手动添加了构造方法(无论有参还是无参),Java就不会再为我们提供默认的无参构造。此时创建对象就必须要传入相应属性,否则就会报错。所以建议重载了有参的构造方法,就手动的将无参构造也写出来。
**封装:**指将一个对象的属性封装在这个对象的内部,不允许外部直接访问对象的内部属性。但是可以提供一些方法来让外部访问和操作这些属性。
**继承:**不同的对象,会有一些共同的点。此时可以把共同的点提取出来封装成一个父类。这样有助于提高代码的复用性。
继承的特点:
**多态:**表示一个对象具有多种状态,具体表现为父类的引用指向子类的对象。
多态的特点:
**浅拷贝:**浅拷贝会在堆中新建一个对象,如果原对象是引用类型的话,将原对象的引用地址复制到新对象中。所以浅拷贝对象和原对象共用一个内部对象
**深拷贝:**深拷贝会完全复制整个对象,包括所有属性和内部对象。
**应用拷贝:**就是两个不同的引用指向同一个对象
public final native Class<?> getClass();//获取当前对象执行的Class对象
public native int hashCode();//通过哈希算法获取哈希值,可以看做是对象的内存地址
public boolean equals(Object obj)//比较两个对象相等
protected native Object clone() throws CloneNotSupportedException;//拷贝一份当前对象并返回
public String toString() {}//将对象转换为String类型
public final native void notify();//唤醒当前正在等待的一个线程
public final native void notifyAll();//唤醒当前正在等待的所有线程
public final void wait() throws InterruptedException {}//暂停线程的执行,wait方法是释放锁的
public final native void wait(long timeoutMillis) throws InterruptedException;
public final void wait(long timeoutMillis, int nanos) throws InterruptedException {//暂停线程的执行并一直执行
protected void finalize() throws Throwable { }//垃圾回收时执行
String
类不可变。**因为String类是被final
修饰的;String中包括保存字符串的byte数组在内的所有属性都是被private
和final
修饰的(在JDK9之前,使用char[]数组存储,之后采用byte[]数组存储);并且String类没有提供给外部修改这个字符串的方法StringBuffer
和StringBuilde
r都继承自AbstractStringBuilder
,AbstractStringBuilde
中也是使用char数组存储,但没有使用final
和private
修饰,并且对外提供了修改字符串的方法。public final class StringBuilder
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
{
abstract class AbstractStringBuilder implements Appendable, CharSequence {
/**
* The value is used for character storage.
*/
char[] value;
线程安全性
String
类不可变,所以是线程安全的。StringBuffer
对父类定义的方法加了同步锁,所以是线程安全的;StringBuilder
对方法并没有添加同步锁,所以是线程不安全的;性能比较
String
每次改变都会新建一个String
对象,性能较低StringBuilder
和StringBuffer
都是对对象本身进行操作。相同情况下,使用StringBuilder
性能仅能提高15%,但却线程不安全。总结:操作少量数据,使用String
;单线程操作大量数据使用StringBuilder
;多线程操作大量数据使用StringBuffer
。
Java不支持运算符重载,但“+”和“+=”专门为String
重载过
StringBuilder
调用append()
方法实现的,拼接完成后,调用toString()
方法转化为String
对象StringBuilder
对象,无法复用字符串常量池是JVM为了提升性能和减少内存消耗对String专门开辟的一块区域,为了避免字符串的重复创建。
在JDK1.7之前,字符串常量池在方法区内;JDK1.7之后,字符串常量池在堆中。
List list = new ArrayList<>();
自定义泛型:泛型类、泛型接口、泛型方法
常用的泛型通配符:
泛型的实际应用
定义一个统一的结果返回类来进行数据传输和前端交互
public class Result<T> {
Class.forName("完整类名");
.这个方法会导致类加载,类加载时静态代码块就会执行。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gnsC6Obk-1645515095263)(C:\Users\sgxcu\AppData\Roaming\Typora\typora-user-images\image-20220221184216303.png)]
Throwable
的两个子类Exception
和Error
Exception
是程序本是可以处理的,Exception
又可以分为 Checked Exception (受检查异常,必须处理) 和 Unchecked Exception (不受检查异常,可以不处理)。
try/catch
处理,否则会报错RuntimeException
及其子类都统称为非受检查异常Error
:Error
属于程序无法处理的错误 ,例如Java 虚拟机运行错误(Virtual MachineError
)、虚拟机内存不够错误(OutOfMemoryError
)、类定义错误(NoClassDefFoundError
)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。Throwable
的常用方法
public String getMessage() {}//异常的简要描述
public String toString() {}//异常发生的详细信息
public void printStackTrace() {}//打印Throwable封装的异常信息
public String getLocalizedMessage() {}//如果子类没有覆盖此方法,会返回和getMessage同样的信息
finally
块:finally
块中的语句大多数情况下一定会执行的!当try/catch
语句块中有return
时,finally
块中的语句会在方法返回前被执行。
finally
块代码不会被执行的情况
在执行finally
被执行之前,如果JVM停止工作,finally
不执行。
程序所在的线程死亡,finally
不执行;
关闭CPU,finally
不执行。
注意:不要在 finally 语句块中使用 return! 当 try 语句和 finally 语句中都有 return 语句时,try 语句块中的 return 语句不会被执行。
总结:序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。
transient
关键字对于不想进行序列化的变量,使用 transient
关键字修饰。
transient
关键字的作用:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient
修饰的变量值不会被持久化和恢复。
注意:
transient
只能 修饰变量,不能修饰类和方法。
transient
修饰的变量,在反序列化后变量值将会被置成类型的默认值。例如,如果是修饰 int
类型,那么反序列后结果就是 0
。
static
变量因为不属于任何对象(Object),所以无论有没有 transient
关键字修饰,均不会被序列化。
InputStream (字节输入流)、OutputStream (字节输出流)、Reader(字符输入流)、Writer(字符输出流)
Stream
结尾的都是字节流。以“Reader/Writer”
结尾的都是字符流。
注:所有的流在使用结束后,都要使用close()方法关闭;所有的输出流在最终输出时候都要使用flush()方法刷新流。
文件专属:FileInputStream、FileOutputStream、FileReader、FileWriter
转换流:(将字节流转换成字符流)InputStreamReader、OutputStreamWriter
缓冲流专属:BufferedReader、BufferedWriter、BufferedInputStream、BufferedOutputStream
数据流专属:DataInputStream、DataOutputStream
标准输出流:PrintWriter、PrintStream
对象专属流:ObjectInputStream、ObjectOutputStream
select
调用,询问内核空间,数据是否准备完毕,确定准备完毕,用户线程才会发起read调用。减少了无效的系统调用。静态代理中,我们对目标对象的每个方法的增强都是手动完成的,非常不灵活(比如接口一旦新增加方法,目标对象和代理对象都要进行修改)且麻烦(需要对每个目标类都单独写一个代理类)。
**从 JVM 层面来说, **静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。
静态代理实现步骤:
定义一个接口及其实现类;
public interface MessageService {
String send(String message);
}
public class MessageServiceImpl implements MessageService{
@Override
public String send(String message) {
System.out.println("发送信息:"+message);
return message;
}
}
创建一个代理类同样实现这个接口
public class MessageProxy implements MessageService{
private MessageService messageService;
public MessageProxy(MessageService messageService) {
this.messageService = messageService;
}
@Override
public String send(String message) {
System.out.println("调用方法前执行");
messageService.send(message);
System.out.println("最后执行的");
return null;
}
}
将目标对象注入进代理类,然后在代理类的对应方法调用目标类中的对应方法。这样的话,我们就可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己想做的事情。
public class ProxyTest {
public static void main(String[] args) {
MessageService messageService = new MessageServiceImpl();
MessageProxy proxy = new MessageProxy(messageService);
proxy.send("Hello Proxy!");
}
}
动态代理更加灵活。我们不需要针对每个目标类都单独创建一个代理类,并且也不需要我们必须实现接口,我们可以直接代理实现类( CGLIB 动态代理机制)。
从 JVM 角度来说,动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。
Java动态代理机制的核心:InvocationHandler
接口和 Proxy
类是核心。
JDK动态代理使用步骤:
定义一个接口及其实现类;
public interface MessageService {
String send(String message);
}
public class MessageServiceImpl implements MessageService{
@Override
public String send(String message) {
System.out.println("发送信息:"+message);
return message;
}
}
自定义 InvocationHandler
并重写invoke
方法,在 invoke
方法中我们会调用原生方法(被代理类的方法)并自定义一些处理逻辑;
* 动态代理
*/
public class DynamicInvocationHandler implements InvocationHandler {
//代理对象
private Object proxy;
public DynamicInvocationHandler(Object proxy) {
this.proxy = proxy;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("调用方法前执行");
Object result = method.invoke(proxy, args);
System.out.println("最后执行的");
return result;
}
}
通过 Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)
方法创建代理对象;
public class JdkProxyFactory {
public static Object getProxy(Object proxy) {
return Proxy.newProxyInstance(
proxy.getClass().getClassLoader(), // 目标类的类加载
proxy.getClass().getInterfaces(), // 代理需要实现的接口,可指定多个
new DynamicInvocationHandler(proxy) // 代理对象对应的自定义 InvocationHandler
);
}
}
测试
MessageService messageService1 =(MessageService) JdkProxyFactory.getProxy(new MessageServiceImpl());
messageService1.send("Hello Spring...");
灵活性 :动态代理更加灵活,不需要必须实现接口,可以直接代理实现类,并且可以不需要针对每个目标类都创建一个代理类。另外,静态代理中,接口一旦新增加方法,目标对象和代理对象都要进行修改,这是非常麻烦的!
JVM 层面 :静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。而动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。
值传递 :方法接收的是实参值的拷贝,会创建副本。
引用传递 :方法接收的直接是实参所引用的对象在堆中的地址,不会创建副本,对形参的修改将影响到实参。
Java 中将实参传递给方法(或函数)的方式是 值传递 :
如果参数是基本类型的话,很简单,传递的就是基本类型的字面量值的拷贝,会创建副本。
如果参数是引用类型,传递的就是实参所引用的对象在堆中地址值的拷贝,同样也会创建副本。
float
或double
运算存在精度损失的问题?因为计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。
计算机在存储小数时候,不断乘以2,当不再存在小数位时候,得到的整数部分就是二进制的结果
需要对浮点数进行精确运算的业务中,都要使用BigDecimal
浮点数之间的等值判断,基本数据类型不能用==来比较,包装数据类型不能用 equals 来判断。
解决方法就是使用BigDecimal
定义浮点数,在进行运算
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.2");
System.out.println(a.add(b));//1.2
System.out.println(a.subtract(b));//0.8
System.out.println(a.multiply(b));//0.20
System.out.println(a.divide(b));//5
BigDecimal
时,为了防止精度丢失,推荐使用它的BigDecimal(String val)
构造方法或者 BigDecimal.valueOf(double val)
静态方法来创建对象。Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。