JavaSE

404 阅读19分钟

Java语言特性?

①简单性:取消了C++的头文件、指针、结构体等,使Java变得更加轻量。

②面向对象:

③分布式:Java支持在网络上分布,支持访问网络中的对象。

④健壮性:Java有编译阶段,编译器就能检测出很多错误

⑤多线程:Java执行多线程的交互执行

⑥可移植性:Java不依赖平台,一次编译随处运行,使用Java编写的程序可以在任何操作系统上运行

⑦垃圾收集:Java通过垃圾收集器回收分配内存,我们不需要自己操心内存的分配和回收。

⑧Java不一定是解释执行的:Java源代码通过Javac编译成字节码,然后在运行时,通过JVM内嵌的解释器将字节码转换成最终的机器码。但是JVM一般都提供了动态编译器,动态编译器能够在运行时把热点代码编译成机器码,那这些热点代码就属于编译执行了。

JRE是Java运行环境,包含了JVM和Java类库。JDK可以看作是JRE的一个超集,提供了更多工具。


创建对象的方式

①new出对象。

②反射创建对象

  • 使用Class对象的newInstance()方法,使用类的默认构造器创建实例。
  • 通过Class对象得到Constructor对象,使用Constructor对象的newInstance()方法,此方法可以指定构造器。

③clone()创建对象。(浅拷贝)

④通过流的反序列化得到对象。(深拷贝)

序列化:
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(obj);
oos.close();

反序列化:
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
cloneObj = ois.readObject();
ois.close();

面向对象

面向对象编程是将对象或类作为代码组织的基本单元来进行编程的一种编程范式。它以对象为程序的基本单元,并将封装、抽象、继承、多态四大特性作为代码设计的基石,提供程序的可重用性。面向对象编程使得我们更加关注于创建出的对象数据,而非创建对象的过程。而面向对象编程语言就是能够方便实现面向对象四大特性的语言。

封装

封装就是提供一种数据访问保护,它限制了属性的作用域,外部仅能通过接口来访问类的内部信息或数据,类就可以通过控制它暴露的函数来管理对数据的访问控制权限与规则,提高了安全性并简化了代码调用,使得我们更加关注与对象结果本身。

我从三个角度来说一下封装:

从类型角度,在早期Java只有几种基础的数据类型,而像字符串等类型都是我们由几种基础数据类型封装和抽象出来的。这表明了我们只需要使用封装好的对象,不必关注对象的内部实现,更加过关注于结果本身。

从生命周期角度,封装在对象中成员变量是与对象的生命周期一致的,对于封装的静态变量等,它们被所有的类实例对象共享,所以与所有对象的生命周期保持一致。这表明了封装可以简化开发,不必使我们过于纠结对象的创建销毁过程,而更加关注于对象本身。 对于封装,我们需要Java提供一定的语法机制来支持,也就是访问控制权限。通过private等关键字,我们可以保护类中的属性不被类外部的代码直接访问。

抽象

封装是隐藏属性与数据,那么抽象就是隐藏方法的具体实现,让调用者只需要关心方法提供了哪些功能,而并不需要知道功能是如何实现的,为我们实现了开闭原则(对扩展开放,对修改关闭)与解耦。

继承

一个类的子类自动具备了父类的属性和方法。

它最大的好处是代码复用,子类可以复用父类提供的属性和方法,父类可以通过继承实现子类型化。子类型的可替换性使得父类型的方法在无需修改的情况下就可以扩展,这也是多态产生的原因。Java不支持多继承。

多态

不同子类对于同一个方法具有不同的实现,那么就可以使得同一个方法通过切换子类来转换相应的结果

多态提高了代码的复用性与可扩展性。而且多态意味着我们站在更高的层次上的抽象,不用在意底层的具体实现,这也体现了面向对象。


接口和抽象类

抽象类

抽象类是一种自下而上的设计思路,先有子类的代码重复,然后抽象成上层的父类,是is-a的关系。抽象类就是为了代码复用,多个子类可以继承抽象类中定义的属性与方法。(子类继承父类也可以实现代码复用的目的,但是对于需要子类重写的方法,父类中只能定义空方法,会影响代码的可读性。而且普通父类可以被实例化,可能出现空方法被误用)。

特性:①抽象类不允许被实例化,只能被继承 ②抽象类可以包含成员变量和方法 ③子类继承抽象类,必须实现抽象类中的所有抽象方法

接口

接口是一种自上而下的设计思路,先根据需求设计接口,然后再考虑接口的多个实现,是has-a的关系。接口更侧重于解耦。接口是对行为的一种抽象,具体的实现代码对调用者透明,实现了约定与实现分离的目的,降低代码之间的耦合度。

特性:①接口不能包含成员变量 ②接口只能声明方法,不能包含代码实现 ③类实现接口,必须实现接口中的所有方法。


谈一谈Java反射、缺点:

反射是Java中提供的运行时可以获取类信息的能力。在运行代码前,我们不确定程序运行后会使用哪一种数据结构或方法,而new对象是在编译期就把对象的类型确定下来,所以我们需要使用反射,反射在程序运行过程中动态地获取类信息和类的方法信息,然后通过反射构造类实例,调用对应的方法。

方法的反射调用Method.invoke(),它实际上是委派给了MethodAccessor来处理,这个MethodAccessor是一个接口,在每个Method实例第一次反射调用时都会为MethodAccessor接口生成一个委派实现。它可以委派到本地实现,我们可以通过本地方法得到Method实例所指向方法的具体地址,那这时候就把传入的参数准备好,然后调用进目标方法即可;它还可以委派到动态实现,直接使用invoke指令来调用目标方法,避免了使用C++的本地方法,性能比本地实现高。所以我们可以关闭反射调用的inflation机制,那么反射就会取消委派实现而直接使用动态实现,提高性能。

反射的使用

①获取类的Class对象:我们通过javac编译.java文件后,就会生成一个字节码文件.class,那字节码文件在装载入JVM后就会在内存中生成Class对象,它包含了该类的所有信息,我们可以通过.class、getClass()、Class.forName()方法获取Class对象。(补充:其中.class是在编译前已经声明了该类的类型,也就是说.class生成的Class对象是有类型的!)

②获取类的实例对象:ClassObj.newInstance()只能调用无参构造器、ClassObj.getConstructor(构造参数.class).newInstance("构造参数")使用其他构造方法反射创建对象。

③Class对象可以获取Field类变量、Method类方法、Constuctor构造器(getDeclaredxxx是获取所有,但无法获取继承父类的信息;getxxx获取public信息,但可以获取父类继承下来的信息)

反射还提供了setAccessible(flas)方法,可以修改变量的成员访问限制。在NIO中需要手动释放DirectBuffer,所以我们可以使用此方法绕开内部API的访问限制。

反射应用场景

①Spring:加载bean反射、IOC反射

②反射工厂模式:通过反射消除工厂中的多个分支,若需要产生新的类,无需关注工厂类,直接传入我们需要的子类类名即可。

反射的缺点

①可以强制访问private修饰的信息,破坏private的封装特性。

②具有一定的性能损耗,需要各种检查步骤和解析步骤。我们可以关闭inflation机制,那么就会减少一点性能损耗。

反射能破坏private,那还要private干嘛

private并不是用来保证安全的,它只是一种封装的思想,做到不暴露内部的实现,使得外界只需要关注于我暴露出的接口等。我平常不需要你private的属性,你自己封装起来方便我使用;但我选择需要你private的属性了,我就直接反射获取,这很河里。


java 重写的条件,重载的条件

重写

在具有继承关系的两个类之间

方法名、参数列表必须相同

子类重写方法的返回值:若为基本类型则必须与父类相同、若为引用类型则需要为父类的返回值或其子类。

重写方法的访问修饰符范围要大于等于被重写方法的访问修饰符 重载: 必须发生在一个类中、方法名必须相同、参数列表必须不同


【Object类方法】

getClass()、hashCode()、equals()、clone()、wait()、notify()、notifyAll()、toString()、

finalize():在对象被垃圾收集前被调用,用于资源回收。它会拖慢垃圾收集导致大量对象堆积,可能导致OOM,所以它在Java9中已经被废弃。可以使用Cleaner来代替finalize,它会利用幻想引用和引用队列进行资源回收


【内部类和静态内部类的区别?static方法能不能被重写?】

内部类的封装性更强,它不允许外部类之外的其他类直接访问。

外部类可以调用内部类的方法,外部类之外的其他类不可用调用内部类的方法。

内部类的实例化需要外部类的实例对象,静态内部类的实例化不需要外部类的对象。

特别的,内部类不能声明静态成员变量,静态内部类不能引用外部类的非静态成员变量。

static可以修饰成员变量、修饰成员方法、静态块(一起初始化),静态方法可以被继承,但是不能被覆盖、不能被重写


【请解释hashCode()和equals()方法有什么联系】

hashCode()方法是获取哈希码,把对象映射到哈希表的索引位置

  • 类中没有对应的哈希表时(HashSet、HashTable...),equals()与hashCode()无关。

-类中有对应的哈希表时,如果两个对象相等,则它们的hashCode()值相同.类中有对应的哈希表时,要判断两个对象是否相等,要重写equals()和hashCode()方法。因为重写equals()意味着两个对象地址不同,但此时认为他们相同。既然相同那么映射到哈希表的位置也应该相同。而如果不重写hashCode(),则它们的hashCode()不同,类中对应的哈希表仍然可以正常添加两个对象,导致此时的类对象实际是不同的,即equals()失效。


【==和equals的区别】

==:它的作用就是判断两个对象的地址是不是相等,当然了对于基本数据类型就是比较的值

equals:对象的equals()若没有被重写,则与==等效。String.equals()默认被重写,是比较对象中的值是否相同。


【String、StringBuilder、StringBuffer的区别】

① String的底层是final static char数组,不可变。每次对String变量修改时,实际上是生成一个新String,然后指针指向新String。

②String使用字符串常量池...

③StringBuilder和StringBuffer都继承AbstractStringBuilder类,底层都是char数组,没有被final修饰,所以都是可变的。

④StringBuilder的方法没有加锁,线程不安全;StringBuffer对方法加锁,线程安全。

⑤StringBuilder和StringBuffer的扩容机制:无参构造默认大小是16,若参数类型为Integer 则初始化大小指定为整型值;若参数类型为String 则初始化大小为 string.length()+16、每次扩容大小为 原数组大小*2 + 2(len<<1 + 2)

【String为什么要设计成不可变呢】

① 同一String实例被多个线程共享时,String不变保证了多线程安全 ② String被类加载机制使用,String不变保证了正确的类被加载。 ③ 字符串常量池的需要:字符串常量池存放字符串对象,程序中会有多个变量指向这个对象。若String可变,则必须连带所有变量都要改变。 ④ HashMap使用String表示哈希值作为key来存储,若字符串可变,则hashcode会变,则对应的value会丢失造成内存泄漏。而且String会缓存hashCode来提高效率,若可变则缓存失效。


【开闭原则是什么】

类、函数等实体对于功能扩展开放,对于修改封闭。就是说我们应该通过扩展实体的行为来应对新的需求,而不是通过修改现有代码来完成。


【final关键字】

final可以用来修饰类、方法、变量。final修饰的类代表不可以被继承扩展,final修饰的变量不可以被修改,final修饰的方法不可以被重写。final关键字可以明确我们代码的语义,final修饰的变量具有不可变效果,即不能对final变量再次赋值,可以减少并发编程下额外的同步开销。

而且在happens-before原则中,final共享变量的构造器(即默认参数赋值)happens-before外界获取final变量的值,这也保证了final变量需要初始值且引用保持不可变。


【Maven相关】

Maven如何编译:mvn compile

Maven如何打包:

mvn package:项目根目录下

mavendependency-plugin插件加mvn package打包:项目指定目录下,并且可以知道main类,就可以直接通过java -jar xxx.jar运行jar包

maven-shade-plugin插件打包


【注解处理器】

注解作用:定义编译规则,并检查被编译的源文件。Java源代码在编译时,先生成抽象语法树,然后会调用已注册的注解,再生成字节码文件。

注解中有另外两个元注解

@Target(Element.METHOD):限定注解能够修饰的Java结构类型

@Retention(RetentionPolicy.RUNTIME):限定注解的生命周期(源代码+字节码+运行时)


【自动装箱和自动拆箱】

public static void main(String[] args)
    {
        Integer a = 1;
        Integer b = 2;
        Integer c = 3;
        Integer d = 3;
        Integer e = 321;
        Integer f = 321;
        Long g = 3L;
 
        System.out.println(c == d);
        System.out.println(e == f);
        System.out.println(c == (a + b));
        System.out.println(c.equals(a + b));
        System.out.println(g == (a + b));
        System.out.println(g.equals(a + b));
    }
    返回:true、false、true、true、true、false

自动装箱:Java编译器把Java基本类型自动转换为包装类型的过程。

比如Integer i = 100,编译器会多执行一步Integer i = Integer.valueOf(100)。

对于-128到127之间的值,Integer.valueOf(i)返回的是缓存的Integer对象,之外的值就是返回的新对象。(所以会牵扯到==与equals的问题,==是要返回false的!)

对于String s1 = "a"+4,实际上会先把"a"自动装箱为StringBuilder,然后append连接,再拆箱为String

自动拆箱:Java编译器把包装类型中的Java基本类型自动提取出来的过程。

比如Integer i;int i = i,编译器会多执行一步int i = i.intValue()。

==的双端一旦出现算数运行(是算数运算啊!),则双端全部拆箱为基本类型。但equals在算数运算拆箱后,会重新装箱。

Integer.valueOf(100)和Integer.parseInt("100")是否相等:

Integer.parseInt()是返回基本数据类型int,而Integer.valueOf()可能返回常量池中的对象、可能返回堆中的对象。而对于100来说,是


【如果系统想对外的话,要怎么操作】:

对外就是要在服务器上发布。买个Linux系统的阿里云服务器,买个域名解析到服务器的IP上。然后使用阿里云开启对应的安全组,放行对应的端口。然后把项目打成jar包,发布到服务器上运行就可以访问了。

SpringBoot项目打包时默认将依赖包也打入项目jar包中,如果想要分开打包就要用assembly插件,然后通过assembly.xml配置文件将两个jar包相关联。

指定使用磁盘的配置文件运行项目:java -jar xxx.jar --spring.config.additional-location=D:/Desktop/application.properties


【序列化与反序列化】

什么是序列化和反序列化

Java对象退出JVM时会被全部销毁,所以我们想在网络传输中保证对象状态的持久化,就要使用序列化将其转换成二进制流,保证对象的状态安全。(对象序列化保存的是对象的状态,所以类的静态变量不会被序列化)。反序列化就是把网络中的二进制流转换为JVM中的Java对象。

如何实现序列化

①对象输出流的writeObject()序列化,对象输入流的readObject()反序列化(对象需要实现Serializable接口)

②Hession序列化协议、Json串、ProtoBuf(需要编写.proto文件,然后用protoc工具类转换成build类库,然后就可以序列化了)

【Listener、Filter、Servlet加载顺序】

Listener → Filter → Servlet(理发师)

【Java泛型】

泛型就是使得我们的对象类型可以像参数那样传递,它只在编译期时生效,并做一个提前的类型检查,避免错误的类型转换在运行时才会报错。当然运行后,会发生泛型擦除。

【Java是值调用还是引用调用】

Java是值调用还是引用调用?方法调用根据传参情况分为值调用和引用调用。

① 值调用():实参仅仅传递它的数值给形参,他俩本身指向的地址不同。

而对于Java而言,无论传递什么数据类型他都是值调用。如果是传递引用类型,则会传递引用本身的地址,所以对形参本身修改不会产生影响,但对形参指向的地址修改,就会修改它们所指向的共同对象。

② 引用调用:实参传递它的指向地址给形参,他俩指向相同的一块内存空间。所以可能会改变对象的内容,但不会改变实参本身。

【String有啥方法?如何扩展String】 ① String有 1) equals()、equalsIgnoreCase()、contains()、startsWith()、toLowerCase() 2) replace()、substring() 3) charAt()、indexOf(“查下标”)、hashcode()、 4) getBytes()、toCharArray()、split(“.”) 5) valueOf():将其他类型转换为string intern():创建字符串后会把其添加到常量池中(若池中有则直接使用),然后返回常量池中的引用。

② 不能继承String。如果要扩展,可以通过组合代替继承,也就是利使一个静态类封装String透出我们的扩展方法;可以使用wrap包装类;也可以使用StringBuilder和StringBuffer。

【IO流了解吗?传输文件需要什么流?】 ① 字节流继承InputStream和OutputStream。有FileInputStream、BufferedOutputStream(包装). ② 字符流继承Reader和Writer。 ③ 文件:FileReader、FileWriter、BufferedReader、BufferWriter。Reader是把数据读入内存。

【了解异常吗?开发中如何处理(极客可以RE)】 异常都是实现了throwable接口,主要分为Error和Exception,Error就是虚拟机内部的错误,程序无法抛出处理,也不需要程序对这些异常进行处理,主要有OutOfMemoryError和StackOverFlowError这两种。Exception主要是运行时异常还有IO异常。运行时异常遇到的比较多,像NullPointer, IllegalArgument, ArrayIndexOutOfBound, ClassNotFound这些。一般会在可能出现异常的代码块会使用try catch finally来捕获异常,然后可以选择在当前类throws出去,或者打印以及输出到日志。一般会用到getMessage或者printStackTrace来记录异常的堆栈信息,方便追踪和调试。

(空指针异常、ClassCastException类型转换异常、IndexOutOfBoundsExecption下标越界异常、BufferOverflowException IO引发的缓冲区溢出异常、NumberFormatException数字格式异常) java.nio.channels.IllegalBlockingModeException:NIO必须先设置channel为非阻塞,才能注册channel连接事件。

【Java基础数据类型】 image.png

在计算机中,数值以二进制存储,所以直接用double加减乘除,计算机会进行十进制和二进制的转化,造成精度缺失。所以使用BigDecimal: 使用BigDecimal时,构造参数一定要传入字符串。 浮点数的字符串格式化,也要先转成BigDecimal。 防止溢出:Math.xxxExact()方法,在溢出时会抛异常

@Data其中包含@EqualsAndHashCode,即默认使用所有的字段进行equals()和hash(),可能会出现业务逻辑错误哦。@EqualsAndHashCode.Exclude可以排除比较字段,且默认不比较从父类继承来的属性。

空值处理:Optional.ofNullable(xxx).orElse(xxx)、Objects.equals(string,string)

异常处理:我们不应该仅仅在框架的MVC三层捕获处理异常,这样有可能导致无法感知Repo的异常、Service异常导致业务功能不正常、重试等。在框架层面使用@RestControllerAdvice+@ExceptionHandler捕获“最终未处理的异常”即可。而对于业务逻辑中的try-catch,业务异常和Error都应该包装并返回给调用方,由调用方做处理。还有就是finally中也有可能抛异常,就会覆盖try中的。所以我们全部使用try-with-resources,他的内部会把两部分的异常整合并展示。对于线程池,若使用execute提交任务,抛异常则会中断当前线程;使用submit提交任务,异常会被保存在FutureTask中,只有get()时才会显现。