Java基础

在回顾Java基础知识的时候做的笔记,有什么用词不准确或者理解错误的地方欢迎大家在评论区指正。参考:JavaLearn

一、Java基础概念

字节码?编译执行 Or 解释执行?

Java是一种先编译、后解释执行的语言。所有的.java文件需要通过javac命令先编译为.class文件,也就是字节码。然后,由JVM从.class文件逐行读取并执行。

Java通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时保留了解释性语言可移植的特点。此外,因为字节码对于不同平台的JVM是通用的,因此Java程序无需重新编译就可以在不同的计算机上运行,也就是“一次编译,到处运行”。

不同版本的JDK

  • OpenJDK是完全开源的,每三个月发布一次。因为会频繁更新以支持其他性能,所以可能会导致不稳定。
  • OracleJDK并非完全开源,每三年发布一次。它相比OpenJDK而言更加稳定,更适合用于企业开发。此外,它的响应性和JVM性能都更好。
  • 国内,阿里的Alibaba DragonWell版本,免费且提供长期支持。

基础数据类型

基本类型字节默认-----基本类型字节默认
byte10-----char2‘u0000’
short20-----float40f
int40-----double80d
long80L-----boolean1bitflase
  • 关于boolean,理论上只占用1bit,但实际应用中会考虑计算机高效存储因素。在JVM中没有提供boolean专用的字节码指令,boolean类型数据会被编译为int类型(4字节32bit),boolean数组会被编译成byte数组(1字节8bit)。

二、关键字

switch能作用在哪几种数据类型上?

首先要明白switch底层的机制,switch底层使用整形int进行判断。

  • Java5之前switch只能作用在byte、short、int、char上,其中byte、short、char可以隐式转换为int
  • 从Java5开始,Java可以作用在枚举类型enum上,enum根据枚举元素在枚举中的序号转换为int
  • 从Java7开始,switch可以作用在String上。String通过hashcode()转换为int,但是可能冲突,此时就需要equals()
  • 同时switch还支持上述类型的包装类型。但是switch不能作用在long上,因为long能表示的范围比int大。

总结:switch支持所有能隐式转换为int的类型以及其包装类型,enum根据序号转换为int,String通过hashcode来转换为int。

final & finally & finalize

  • final:fianl修饰的变量引用不可变,final修饰的方法子类可以使用但不能被重写,final修饰的类无法继承且内部方法无法重写。

引用不可变的变量,只能被赋值一次。但是如果变量是对象,其属性值可以变化。final修饰的全局变量必须立即初始化,而修饰的局部变量可以不立即初始化。

String以及所有的包装类的对象都是不可变对象,任何的修改都会创建一个新的对象。不可变对象的最大好处是线程安全。

  • finally:作为异常处理的一部分,只能用于try/catch语句中,表示最终一定被执行。System.exit(0)可以阻断finally的执行。
  • finalize:是Object类内的方法,因此所有的对象都有这个方法。此方法在gc时启动,对象被回收时调用。一个对象的finalize只会被调用一次,但是被调用不一定会立即回收该对象。可能调用后该对象又不需要被回收了,而到了真正需要回收的时候,因为已经调用过一次所以不能再调用finalze,因此不推荐使用。

static关键字

通常用new创建类对象时,才会分配数据存储空间,类内方法才能被调用。但static修饰的成员变量和方法可以在所属类没有实例的情况下被访问。

static方法是不能被覆盖的,因为方法的覆盖是基于运行时动态绑定。而static方法是编译时静态绑定的,它跟类的任何实例都不相关,概念上不适用。

动态绑定:在程序运行过程中,根据具体的实例对象来确定具体用哪一个方法。

在继承中代码的执行顺序:
在这里插入图片描述

三、面向对象

面向对象的三大特性

  • 封装:将客观事物封装成抽象的类,并且类可以把自己的数据和方法仅让信任的类或对象进行操作。
  • 继承:可以使用现有类的所有功能,且能够在不改动原有类的情况下对这些功能进行拓展,可以帮助我们更好的遵守OCP原则(开闭原则)。通常把被继承的类称为父类,通过继承创建的类叫做子类。
  • 多态:父类中定义的属性和方法被子类继承后,可以具有不同的数据类型或者表现出不同的行为,这使得同一个属性或方法在父类和子类中具有不同的含义。

Java如何实现多态

多态分为编译时多态和运行时多态,又称静态多态和动态多态。重载overload就是编译时多态,也就是说编译的时候就已经确定了运行的时候要调用的方法。

在Java中,我们说多态,通常都是指运行时多态,也就是编译时不确定要调用的是哪一个方法,一直延迟到运行时才能够确定。这也是多态方法又被称为延迟方法的原因。

Java实现多态需要有三个必要条件:继承、重写、向上转型。

  • 继承:在多态中必须存在有继承关系的子类和父类
  • 重写:子类需要对父类的某些方法进行重新定义,这样在调用时就会调用子类的方法
  • 向上转型:父类引用指向子类对象

重载和重写

overload和override都是实现多态的方式,区别如下。

  • overload:编译时多态,发生在一个类的内部。方法名必须相同,方法参数必须不同,返回类型可以相同也可以不同。最常用的overload就是构造器的重载,无参构造、全参构造等等。
  • override:运行时多态,发生在父类和子类间。方法名、返回值和参数都不能改变。也就是外壳不变,核心重写

构造器可以被重写吗?

构造器不能被继承,因此不能被重写,但是可以被重载,比如无参构造和全参构造。每个类必须有输入自己的构造函数,子类不会覆盖掉父类的构造函数,相反的,必须一开始调用父类的构造函数。

抽象类和接口

从语法层面来说:

  • 抽象类可以提供成员方法的实现细节,接口只能是public abstract方法
  • 抽象类成员变量可以是各种类型,接口成员变量只能是public abstract final类型
  • 抽象类可以有静态代码块和静态方法,接口不可以
  • 一个类只能继承一个抽象类,但是能实现多个接口

从设计层面来说:

  • 抽象类是自下而上的,先有子类,抽取其共同点获得抽象类。它是对类整体的抽象。
  • 接口是自上而下的,先由接口来设计规范,然后由子类来实现。它是对类局部、也就是类的行为的抽象。

面向对象 & 面向过程

  • 面向对象易维护、易复用、易拓展,由于封装、继承、多态等特性的存在,可以设计出更加灵活且低耦合的系统。但是因为类在调用的时候需要实例化,开销较大,更加消耗资源。
  • 面向过程性能比面向对象要好,所以单片机、嵌入式开发等性能要求较高的场景下一般采用面向过程开发。但是,面向过程的维护性、拓展性、复用性不如面向对象。

值传递 & 引用传递

  • 值传递:方法调用时传递的参数是值的拷贝,传递之后参数与原来的值互不相关
  • 引用传递:方法调用时传递的是参数对应的值的引用,也就是值的地址。也就是说传递的是值的地址,传递前后都指向同一个引用。

在Java中只有值传递。首先,基本类型传递的时候肯定是值传递,关键是引用类型。在Java中,引用类型作为参数也是值传递,只不过这个值是引用的地址。也就是说,会现将地址复制一份,然后用作参数。这个跟引用传递还是比较容易混淆的。

四、对象相等

equals & ==

  • ==通常用于比较基本数据类型,两者值相同返回true;用于比较两个对象,对象地址相同返回ture。
  • equals通常用于两个对象之间,看一个对象是否等于另一个对象。但是如果当前类没有重写equals方法,那就会执行Object类中的equals,也就是直接调用==
    在这里插入图片描述

hashCode()

hashCode()可以获取一个对象的哈希码,也叫散列码,返回的是一个整数。哈希码的作用是确定该对象在哈希表中的索引位置。hashCode()是Object类中的方法,因此所有的对象都可以使用这个方法。

哈希表存储的是键值对,能根据键快速检索到对应的值,应用到了哈希码。

hashcode很重要,以HashSet为例。当要将对象存入HashSet时,HashSet会先计算对象的hashcode来判断对象加入的位置,如果HashSet不存在此hashcode,HashSet会假设对象没有重复出现,直接存入。如果此hashCode已经存在,会调用equals()来判断两个对象是否真的相同。如果相同,就不会存入此对象。如果不同,重新散列此对象。

为什么要用hashCode()和equals()的组合

如果单单使用equals()来判断对象是否相等,如果集合内存在1000个元素,就需要将1000个元素一个一个与目标元素相比较,效率较低。

如果单单采用hashcode(),可以很快速的缩小需要比较的范围。但是不同的对象也可能有相同的hashcode,因此hashcode并不能保证准确性。

所以我们需要使用hashcode()equals()的组合,既保证了速度,又保证了准确性。先用hashcode()缩小需要比较的范围,然后用equals()比较两个对象是否相等。

为什么重写equals()时必须重写hashcode()

可以看一下源码中Object类里面hashcode()的注释部分,里面有一句话:

Whenever it is invoked on the same object more than once during an execution of a Java application, the {@code hashCode} method must consistently return the same integer, provided no information used in {@code equals} comparisons on the object is modified.

也就是说,对于一个对象,如果这个对象内的所有被equals()方法用到的信息没有被修改,那么hashcode()就应该返回同一个int值。

equals()hashcode()本身就是配套使用的,在判断的时候会根据hashcode()的结果来决定是否需要调用equals()。Object类本身内部的equals()是直接调用==,如果只重写equals()而不重写hashcode(),会造成hashcode值不同,而equals()判断出来的结果为true。

比如Java的一些容器中,不允许有两个完全相同的对象。如果只重写了equals(),那么这两个对象的hashcode()就直接采用==,也就是根据对象的存储地址转换形成一个哈希值,就会导致存入两个完全相容的对象。

五、字符串

String & StringBuilder & StringBuffer

  1. 就对象是否可变而言,String类的对象是不可变的,另外两者的对象是可变的。这三个类都用char数组来存储字符串,但是String类的char数组使用final修饰的,任何对已经存在的String对象的修改都会重新创建一个对象。StringBuilder和StringBuffer都继承自AbstractStringBuilder,其内部的char数组没有用final修饰,因此对象是可变的。其实理论上来讲,String在新字符串长度小于原有长度时是可以不新建对象的,因为final修饰的对象只是引用不可变。但是Java中并没有这样实现,这有着性能和安全性上的考虑。
  2. 从线程安全的角度,String对象不可变,相当于常量,显然线程安全。StringBuffer通过用synchronized修饰方法来实现线程安全。StringBuilder非线程安全。
  3. 就性能而言,StringBuilder比StringBuffer通常性能高10%-15%。通常要操作少量的数据用String,单线程下操作大量的数据用StringBuilder,多线程操作大量的数据用StringBuffer。

String为什么要设计成不可变的

String设计为不可变是出于多方面的考量。

  1. 在设计方面。Java中会大量使用String常量,如果每次都创建一个新的String对象将会造成极大的空间浪费。JVM为了提升性能并减小内存开销,提出了StringPool的概念。它是一个HashTable,是在堆中专门开辟出的一块用于存储字符常量的空间。如果初始化一个String对象,在StringPool中已经存在,就不会创建新的对象,而是返回已经存在的对象的引用,如下面代码所示。如果String是可变的,StringPool就没办法实现。
    String str1 = "a";
    String str2 = "a";
    System.out.println(str1 == str2); //true
  1. 出于安全性的考虑,包括线程安全和网络安全。
  2. 在性能方面,由于String不可变,保证了hashcode的唯一性,在创建String对象时就可以缓存其hashcode值。这也是Map通常用String作为key的原因,处理速度要快过其他的键对象。

intern()

intern()在jdk1.6和1.7及以后有着不同的处理。注意s.intern()会返回一个地址,但是对于s是没有任何影响的。

  • 在jdk1.6中,s.intern()会先判断字符串常量池中是否存在字符串s
    • 如果存在,直接返回该常量s
    • 如果不存在,在字符串常量池中建立该常量,并返回
  • 在jdk1.7中,s.intern()先判断字符串常量池中是否存在字符串s
    • 如果存在,直接返回该常量s
    • 如果不存在,说明字符串常量s在堆中,则将堆中该对象的引用加入到常量池中;以后拿到的都是堆中该字符串常量的引用

六、反射机制

为什么要存在反射机制?

反射的主要作用:

  • 反射可以通过类的全路径名来创建对象。
  • 反射可以枚举出类的所有成员,包括构造器、属性、方法。

个人觉得反射最大的意义就是我们在只知道类全路径名的情况下访问其所有成员,包括私有成员。

反射的优缺点

优点:

  • 能够动态创建对象,增加程序的灵活性,适合用在一些灵活性、扩展性要求较高的框架上
  • 能够提升代码的复用率
  • 对于任意类能够访问其所有成员,对于任意对象能够调用其任意方法

缺点:

  • 反射会带来性能上的问题。可以简单的通过新建数大量的简单对象来测试运行时间来进行验证,反射耗费的时间是直接代码的数万倍。这是因为反射基本是一种解释操作,我们告诉JVM需求,然后由JVM来满足我们的要求。这类操作的效率总是低于直接执行相同的操作,所以我们应该避免在经常被执行的代码或者是对于性能要求较高的程序中使用反射。
  • 安全问题。反射允许代码执行一些通常情况下不允许的操作,比如从外部访问类的私有属性或方法。这可能会导致一些安全性问题。
  • 反射会模糊程序的内部逻辑。应用反射的代码比对应的直接代码更加复杂,且难以理解其内部逻辑,可能会带来维护上的问题。

七、序列化

什么是序列化与反序列化

Java的序列化就是将Java对象转化为字节序列的过程,反序列化就是将字节序列恢复为Java对象的过程。拿游戏来类比,序列化其实很像游戏的存档,将一个个角色对应的对象存储到硬盘。同样的,反序列化也就对应从存档中读取游戏数据的过程。

  • 序列化:将对象转换为有序字节流,便于网络传输或者本地存储。序列化最核心的作用就是对象状态的保存与重建。正常情况下,Java对象时保存在JVM的堆中的。所以只有JVM在运行的时候,对象才存在。而序列化可以让我们在JVM停机的情况下也能把对象保存下来,变成可存储或可运输的形态。当再次需要此对象时,从文件中读取并反序列化出对象。
  • 反序列化:从网络或文件中获取序列化后的对象字节流后,根据字节流中保存的对象状态及描述信息,通过反序列化重建对象。

为什么需要序列化与反序列化

将对象序列化无非两个目的,一个是用于网络传输,一个是对象的持久化保存

那么问题又来了,为啥对象会需要网络传输或者是持久化的保存呢?可以结合下面的应用场景理解一下:

  1. 现在分布式系统很火对吧,也就是不同的服务部署在不同的服务器上面。如果一个服务器上的对象需要用到部署在另一台服务器上的服务,就可以将对象序列化通过网络传输,然后在远程主机上运行,也就是远程调用。远程调用的核心就是序列化与反序列化。
  2. 序列化还可以将内存中已经实例化的类转换为文件进行存储,下次需要实例化的时候直接反序列化后即可使用。个人觉得这个主要是为了在程序中断后保存对象状态信息,因为序列化和反序列化的速度并不快,尤其是Java自带的,这也是有那么多序列化工具的原因。

序列化的方法

Java可以通过实现SerializableExternalizable接口来实现序列化。可序列化接口的所有子类型都是可以序列化的,因为父类引用可以指向子类实现,比如People people = new Woman();

  • Serializable:此接口内部没有方法或者字段,仅仅用于表明当前类可进行序列化。
  • Externalizable:此接口继承自Serializable,内部定义了writeExternal()readExternal()两个方法。使用当前接口进行序列化与反序列化需要重写这两个方法,否则所有的属性都会变成默认值。

两者相比之下,Serializable易于实现,仅需实现接口,系统会自动存储必要信息,但是性能较差;Externalizable性能较好,但是需要程序员来重写接口内的两个方法,决定存储哪些信息。

serialVersionUID

Java的序列化机制通过serialVersionUID来验证版本一致性。

在不显式指定serialVersionUID的情况下,JVM在序列化时会根据属性自动生成。显式指定后,序列化和反序列化用的就是我们指定的serialVersionUID。

在开发中我们的类不可能一成不变,是需要进行迭代的。一旦类被修改,旧对象的反序列化就会出错。因此我们在开发过程中最好显式指定serialVersionUID,值是多少无所谓,重要的是不能变。那么什么时候serialVersionUID可以修改呢?

《阿里巴巴开发手册》:如果序列化类新增属性,不要修改serialVersionUID字段;如果是不兼容升级,为了避免反序列化混乱,那么请修改serialVersionUID值。

transient关键字

对于类中不想序列化的变量,可以用transient进行修饰。加上transient关键字可以避免变量被序列化到文件中,反序列化后变量的值会被设置为默认值。transient只能修饰变量,不能修饰类和方法。

静态变量会被序列化吗?

不会。

序列化是针对对象而言的,静态变量是先于对象存在的,随着类的加载而加载。serialVersionUID也是用static修饰,所以序列化时也不会序列化,而是JVM在序列化时生成一个serialVersionUID,然后用我们显式设置的值来赋值。

八、异常

Exception 和 Error

ExceptionError都继承自Throwable类。这两者都是Java异常处理的重要子类,各自都包含大量子类。

  • Exception:异常,程序本身可以进行处理。遇到异常,需要通过catch进行捕获,或者用throwthrows做抛出处理,以便程序能够正常运行。Exception又可以分为运行时异常(非受检查异常)和非运行时异常(受检查异常)。运行时异常是RuntimeException及其子类,表示JVM运行期间可能出现的异常,比如NullPointException(空指针异常)IndexOutOfBoundsException(数组越界)ClassCastException(类转换异常)NumberFormatException(字符转换为数字)。受检查异常能够被Java编译器检查出来,常见的有ClassNotFoundExceptionSQLException,以及IO相关的一些异常。
  • Error:错误,仅靠程序自身无法处理。比如,系统崩溃、内存不足、堆栈溢出等等。

throw 和 throws

  • throw:用于方法内部,只能抛出一种异常,运行时异常和非运行时异常都可以抛出。
  • throws:关键字用在方法声明上,可以抛出多种异常,用于标识当前方法可能抛出的异常列表。如果一个方法用throws标注了可能抛出的异常列表,调用此方法时必须包含可处理对应异常的代码,或者同样用throws将异常抛出。

NoClassDefFoundError 和 ClassNotFoundException

NoClassDefFoundError是一个Error,是由JVM引起的,因为JVM或ClassLoader在尝试加载某类时在内存中找不到该类的定义,可能是类在编译后被删除了。

ClassNotFoundException是一个受检查异常,需要我们使用try/catch进行显式的捕获和处理,或者使用throws将异常抛出。当使用Class.forName()动态加载类到内存中时,通过传入的类路径没有找到对应类,就会抛出此异常;另一种出现此异常的情况是某类已经被一个加载器加载到内存中后,另外一个加载器又尝试加载该类。

九、IO

字节流 和 字符流

当读写文件时需要对内容进行处理就选用字符流;仅读写文件,与内容无关,选用字节流。

  • 字节流:字节流按字节进行读写,字节(Byte)是计算机表示信息含义的最小单位,适合处理所有类型的文件。
  • 字符流:字符流按照字符进行读写,只能够处理纯文本数据,但是处理文本相比字符流更加方便。

BIO 和 NIO 和 AIO

  • BIO:Blocking IO,同步并阻塞,在服务器中实现模式为一个连接一个线程。每当用户发起连接请求,服务器就需要启动一个线程进行处理。如果连接闲置,就会造成不必要的开销。当然,可以通过线程池机制来进行改善。BIO通常适用于连接数目少且固定的架构,对于服务器资源要求较高,且并发局限于应用中。不过程序直观简单,容易理解。

  • NIO:Non-blocking IO,同步并非阻塞,在服务器中的实现模式为一个请求一个线程。客户端的所有连接都会注册到多路复用器上,当多路复用器轮训到连接有IO请求时才会启动一个线程进行处理。NIO通常适用于连接比较多且连接比较短的架构,并发局限于应用内部,编程比较复杂。

  • AIO:Asynchronous IO,异步并非阻塞,在服务器中的实现模式为一个有效请求一个线程。客户端所有的IO请求都会先经过操作系统处理后,再通知服务器启动线程进行处理。AIO通常适用于连接较多且连接比较长的架构,充分调动操作系统参与并发,编程比较复杂。

Java IO中的设计模式

  • 适配器模式:把一个类的接口变换成客户端期望的另一种接口,从而使两个原本接口不匹配的类能在一起工作。
    Reader reader = new InputStreamReader(inputStream);

在这里插入图片描述

  • 装饰器模式:动态的往一个类中添加新的行为的设计模式。就功能而言,装饰器模式比生成子类更加灵活,可以给某个对象而不是整个类添加一些功能。
    new BufferedInputStream(new FileInputStream(inputStream));
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值