Java基础知识点

面向对象三大特征

  1. 封装

    • 对外隐藏复杂的实现,暴露出简单的使用方法
    • 可以隔离变化,内部的变化外部不知道
    • 提高代码重用性
    • 保护数据
  2. 继承

    • 提高代码重用性(如果仅仅是为了重用,则优先考虑组合)
    • 多态的前提
  3. 多态

    • 前提:继承
    • 作用:提高代码的扩展性
    • 体现:向上转型
    • 限制:向上转型时,子类独有的成员无法使用

继承和实现都体现了传递性。定义如下:

继承:如果多个类的某个部分的功能相同,那么可以抽象出一个类出来,把他们的相同部分都放到父类里,让他们都继承这个类。

实现:如果多个类处理的目标是一样的,但是处理的方法方式不同,那么就定义一个接口,也就是一个标准,让他们都实现这个接口,各自具体实现自己的处理方法来处理那个目标

其中多态有三个必要条件:有类的继承或接口实现、子类重写父类的方法、父类引用指向子类的对象。另外,还有一种说法,多态还分为动态多态和静态多态,前面提到的编译期和运行期的变化属于动态多态,还有一种静态多态,认为 Java中方法的重载是一种静态多态,因为需要在编译期决定具体调用哪个方法。我认为多态应该体现的是一种运行期特性,比如方法的重写算是多态的一种体现,所以我更偏向于重载不是多态。

Java 的继承与组合

继承(Inheritance)是一种联结类与类的层次模型,继承是一种is-a关系。组合(Composition)体现的是整体与部分、拥有的关系,即has-a的关系。

如何选择?

建议在同样可行的情况下,优先使用组合而不是继承。
因为组合更安全,更简单,更灵活,更高效。

注意,并不是说继承就一点用都没有了,前面说的是【在同样可行的情况下】。有一些场景还是需要使用继承的,或者是更适合使用继承。

继承要慎用,其使用场合仅限于你确信使用该技术有效的情况。一个判断方法是,问一问自己是否需要从新类向父类进行向上转型。如果是必须的,则继承是必要的。反之则应该好好考虑是否需要继承。《Java编程思想》

只有当子类真正是超类的子类型时,才适合用继承。换句话说,对于两个类A和B,只有当两者之间确实存在is-a关系的时候,类B才应该继承类A。《Effective Java》

为什么说Java中只有值传递

很多资料上都说:”Java的参数传递机制只有值传递,没有引用传递。“值传递就是在方法调用时,形参接收的值只是实参的一个副本,这个参数在内部发生变化不会对原始实参产生影响。

对于基本类型的参数而言,参数传递是值传递是没有疑问的,有疑问的是引用参数的传递,就是为什么我一个对象传到一个方法里面,方法结束后原来对象的属性就变了,这是否是有违Java值传递的机制?

引用类型参数(如对象)也按值传递给方法。这意味着,当方法返回时,传入的引用仍然引用与以前相同的对象。但是,如果对象字段具有适当的访问级别,则可以在方法中更改这些字段的值。《The Java™ Tutorials》

也就是说,Java会将对象的地址的拷贝传递给被调函数的形式参数,这依然是值传递。

但是怎么理解在对象参数的传递过程,方法对原来对象产生了影响呢?

这就好比:你有一把钥匙,你复制了一把钥匙给你的朋友,你朋友拿这把钥匙去你家把你家东西给拿走了。在这个过程中,对你手里的钥匙来说,是没有影响的,但是你钥匙对应的房子里的东西却被人改变了。

也就是说,Java对象的传递,是通过复制的方式把引用关系传递了,如果我们没有改引用关系,而是找到引用的地址,把里面的内容改了,是会对调用方有影响的,因为大家指向的是同一个共享对象。

如果说,我们在方法内,再次new一个对象,用引用类型的方法入参接收这个对象,那么在方法内不管对这个新的对象做什么,都不会影响方法外的那个实参对象,因为此时方法体内的那个形参指向的是另一个地址。

这就好比:你把你的钥匙复制一份给你的朋友,但你的朋友把这个钥匙给改了,改成了他家自己家的钥匙,这个时候,他对自己家的房子做什么操作都不影响你的房子。

所以,Java中的对象传递,如果是修改引用,是不会对原来的对象有任何影响的,但是如果直接修改共享对象的属性的值,是会对原来的对象有影响的。

Java基本数据类型注意事项

1、Java为什么设计基本数据类型?

首先基本数据类型使用频繁,如果像创建对象那样频繁去new一个基本类型数据,就会很笨重,比较耗费资源。基本数据类型的变量不会在堆内存中创建而是直接存储在栈内存中,因此使用起来更加高效。

2、那为什么又要有包装类呢?

毫无疑问,Java是一种面向对象的编程语言,很多地方都需要用到对象而不是基本数据类型。比如集合中就无法将基本数据类型放进去的,因为集合的泛型要求是Object类型。为了让基本数据类型具有对象的特征,就有了包装类型,使得它具有对象的性质,并且为其添加了属性和方法,丰富了基本类型的操作。

3、自动装箱与拆箱的实现原理

通过反编译可发现,自动装箱使用的是包装类型的valueOf()方法比如(Integer.valueOf(1)),自动拆箱使用的包装类型的xxxVaue方法(比如var.intValue())。

4、阿里巴巴Java开发手册关于三目运算符空指针风险问题

三目运算符中,如果问号后面的条件表达式一个是基本类型,一个是包装类型,就会触发自动拆箱,这时候,如果包装类型变量为空,就会产生NPE。比如:

 boolean flag = true;
 Integer i = 0;
 int j = 1;
 int k = flag ? i : j;

如果其中的inull,就会报空指针。

5、自动拆装箱带来的问题

自动拆装箱是一个很好的功能,大大节省了开发人员的精力,不再需要关心到底什么时候需要拆装箱。但是,他也会引入一些问题

包装对象的数值比较,不能简单的使用 ==,虽然 -128 到 127 之间的数字可以,但是这个范围之外还是需要使用 equals 比较。

由于自动拆箱,如果包装类对象为 null ,那么自动拆箱时就有可能抛出 NPE。

如果一个 for 循环中有大量拆装箱操作,会浪费很多资源

String

我们都知道,String是Java中一个不可变的类,所以他一旦被实例化就无法被修改。

不可变类的实例一旦创建,其成员变量的值就不能被修改。这样设计有很多好处,比如可以缓存hashcode、使用更加便利以及更加安全等。

既然字符串是不可变的,那么字符串拼接又是怎么回事呢?

其实,所有的所谓字符串拼接,都是重新生成了一个新的字符串。即使拼接了一个新的字符串,也是引用变量指向了一个新的堆内存区域,原来的字符串对象依旧在堆中没有变化。

使用+拼接字符串的实现原理?

根据反编译可以看到,Java中的+对字符串的拼接,其实现原理是使用StringBuilder.append,而StringBuilder最后的toString方法也是新new了一个字符串对象。

字符串常量池

在JVM中,为了减少相同的字符串的重复创建,为了达到节省内存的目的。会单独开辟一块内存,用于保存字符串常量,这个内存区域被叫做字符串常量池。

当代码中出现双引号形式(字面量)创建字符串对象时,JVM 会先对这个字符串进行检查,如果字符串常量池中存在相同内容的字符串对象的引用,则将这个引用返回;否则,创建新的字符串对象,然后将这个引用放入字符串常量池,并返回该引用。

这种机制,就是字符串驻留或池化。

intern()

Stringintern()方法,在调用时,JVM会去字符串常量池检测是否已存在该字符串,如果存在则直接返回该引用;否则在常量池中添加新的并返回引用。

👉 String知识点整理

Java中各种关键字

transient

Java语言的关键字,变量修饰符,如果用transient声明一个实例变量,当对象存储时,它的值不需要维持。这里的对象存储是指,Java的serialization提供的一种持久化对象实例的机制。当一个对象被序列化的时候,transient修饰的变量的值不包括在序列化的表示中,然而非transient修饰的变量是被包括进去的。使用情况是:当持久化对象时,可能有一个特殊的对象数据成员,我们不想用serialization机制来保存它。为了在一个特定对象的一个域上关闭serialization,可以在这个域前加上关键字transient。

简单点说,就是被transient修饰的成员变量,在序列化的时候其值会被忽略,在被反序列化后, transient 变量的值被设为初始值, 如 int 型的是 0,对象型的是 null。

Java 注解

注解能被用来为程序元素(类、方法、成员变量等)设置元数据。注解是一个接口,注解不影响程序的运行。只有通过某种配套工具对注解中的信息进行访问和处理,才能使注解起作用,这种工具统称为 APT(Annotation Processing Tool)。注解本质是一个接口,它继承了java.lang.annotation.Annotation这个接口。

基本注解

  • @Override
  • @Deprecated
  • @SuppressWarnings(抑制编译器警告)
  • @SafeVarages(Java7新增)
  • @FunctionalInterface:Java8新增,被修饰的接口是一个函数式接口,即有且仅有一个抽象方法。

Java 元注解

就是用来修饰注解定义的,包含有

  • @Retention
  • @Target
  • @Documented
  • @Inherited

一旦在注解里定义了成员变量后,使用该注解时就要为其成员变量赋值。 当注解定义的成员变量名为value时,不需要使用value=name的格式,可以注解在括号中指定value值。

@Retention

只能用来修饰注解定义,用于指定被修饰的注解可以保留多长时间。 它有一个RetentionPolicy类型的value成员变量,所以使用必须为value赋值。

  • RetentionPolicy.CLASS:编译器把注解记录在class文件中,JVM不可获取信息
  • RetentionPolicy.RUNTIME:编译器把注解记录在class文件中,程序可通过反射获取该注解信息
  • RetentionPolicy.SOURCE:注解只保留在源码中,编译器直接丢弃这种注解,如:
@Retention(value = RetentionPolicy.RUNTIME)
public @interface Testable {
}

@Target

用于指定被修饰的注解定义,可以修饰哪些程序单元。@Target有一个ElementType数组类型的value成员变量。使用时需要指定value的值。

@Target(ElementType.METHOD)
public @interface Testable {
}

表示@Testable注解只能修饰方法。

@Document

表示被该元注解修饰的注解类将被javadoc工具提取称文档。

@Inherited

被修饰的注解具有继承性。
如,@Inherited修饰了@Testable注解,那么被@Testable注解修饰的类的子类,也会被@Testable注解修饰,简言之,@Inherited@Testable具有继承性。

自定义注解

用关键字:@interface。注解的语法类似于修饰符,通常用来修饰类、接口、方法、变量等。 格式:

public @interface 注解名称 {
  属性列表
}

属性返回值可以是

  • 基本数据类型
  • String
  • 枚举
  • 注解类型
  • 以上类型的数组类型

根据是否存在成员变量,可以把注解分为两类

  • 标记注解:没有成员变量,本身起标记作用,如@Override
  • 元数据注解:包含成员变量的注解,可提取注解信息

反射

反射是Java语言的一个特性,它允许程序在运行时(注意不是编译的时候)来进行自我检查并且对内部的成员进行操作。例如它允许一个Java类获取它所有的成员变量和方法并且显示出来。

反射是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为Java语言的反射机制。

简而言之,将类的各个组成部分(成员变量、构造器、成员方法等)封装成对象,就是反射机制。

反射机制的作用

1、在运行时判断任意一个对象所属的类;

2、在运行时获取类的对 象;

3、在运行时访问Java对象的属性,方法,构造方法等。

什么要用反射机制

这就涉及到了动态与静态的概念。

静态编译:在编译时确定类型,绑定对象,即通过。

动态编译:运行时确定类型,绑定对象。动态编译最大限度发挥了Java的灵活性,体现了多态的应用,有助降低类之间的藕合性。

反射机制的优缺点

优点:可以实现动态创建对象和编译,体现出很大的灵活性,通过反射机制我们可以获得类的各种内容,进行反编译。对于JAVA这种先编译再运行的语言来说,反射机制可以使代码更加灵活,更加容易实现面向对象。

缺点:对性能有影响。使用反射基本上是一种解释操作,我们可以告诉JVM,我们希望做什么并且让它满足我们的要求。这类操作总是慢于直接执行相同的操作。

反射原理

Java在编译后会生成一个.class字节码文件,反射是通过字节码文件找到其类的成员变量、方法、构造器、接口等。最关键的就是字节码对象Class

获取Class对象的三种方式:

  1. Class.forName("全限定类名"),多用于读取配置文件中的类名,如数据库驱动
  2. 类名.class,多用于参数的传递,如工厂模式
  3. 对象.getClass(),调用的是ObjectgetClass()方法,多用于对象获取字节码对象

同一个字节码文件(.class)在一个程序运行过程中,只会被加载一次,上面的三种方式获取的Class对象都是同一个。

反射应用场景

典型的像动态代理,Spring配置文件的初始化。

序列化与反序列化

序列化是将对象转换为可传输格式的过程。 是一种数据的持久化手段。一般广泛应用于网络传输与RMI(远程方法调用)等场景中。序列化是将对象的状态信息转换为可存储或传输的形式的过程。一般是以字节码或XML格式传输。而字节码或XML编码格式可以还原为完全相等的对象。这个相反的过程称为反序列化。

泛型

泛型的本质是为了参数化类型。即在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参不同的类型,在实际应用中,一个类型被变成一个参数可以应用到类、方法、接口上,我们称之为泛型类、泛型方法或泛型接口。

泛型的作用是什么?

适用于多种数据类型执行相同的代码。或者说,一套数据结构通过引入泛型,支持多种类型的参数都执行相同的代码逻辑,最终起到代码复用的作用。

什么是泛型类型擦除?

类型擦除可以简单的理解为将泛型Java代码转换为普通Java代码。
类型擦除的主要过程如下:

  1. 将所有的泛型参数用其最左边界(最顶级的父类型)类型替换。
  2. 移除所有的类型参数。

Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。

泛型的类型擦除原则

消除类型参数声明,即删除<>及其包围的部分。

根据类型参数的上下界推断并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符或没有上下界限定则替换为Object,如果存在上下界限定则根据子类替换原则取类型参数的最左边限定类型(即父类)。为了保证类型安全,必要时插入强制类型转换代码。

自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性”。

上下界限定符extends和super

例子:

public class Food {}
public class Fruit extends Food {}
public class Apple extends Fruit {}
public class Banana extends Fruit{}

public class GenericTest {

    public void testExtends(List<? extends Fruit> list){

        // 代码报错,extends为上界通配符,只能取值,不能存放
        // 因为Fruit的子类不只有Apple还有Banana,这里不能确定具体的泛型到底是Apple还是             
        // Banana,所以放入任何一种类型都会报错
        list.add(new Apple());

        // 可以正常获取
        Fruit fruit = list.get(1);
    }

    public void testSuper(List<? super Fruit> list){

        //super为下界通配符,可以存放元素,但是也只能存放当前类或者子类的实例,
        //以当前的例子来讲,无法确定Fruit的父类是否只有Food一个(Object是超级父类)
        //因此放入Food的实例编译不通过
        list.add(new Apple());
        //list.add(new Food());

        Object object = list.get(1);
    }
}

在使用泛型时,添加元素时用super,获取元素时,用extends。

频繁往外读取内容的,适合用上界Extends。经常往里插入的,适合用下界Super。

原始类型 List 和 Object 类型 List< Object > 之间的区别?

原始类型List和带参数类型List<Object>之间的主要区别是,在编译时编译器不会对原始类型进行类型安全检查,却会对带参数的类型进行检查。通过使用Object作为类型,可以告知编译器该方法可以接受任何类型的对象,比如String或Integer。它们之间的第二点区别是,你可以把任何带参数的类型传递给原始类型List,但却不能把List<String>传递给接受 List<Object>的方法,因为会产生编译错误。

List<?>和 List< Object > 之间的区别?

List<?> 是一个未知类型的List,而List<Object> 其实是已知的但是任意类型的List。你可以把List<String>, List<Integer>赋值给List<?>,却不能把List<String>赋值给 List<Object>

I/O流

按流的流向来分:

  • 输入流:只能从中读取数据,不能向其写入数据;
  • 输出流:只能向其写入数据,不能从中读取数据。

输入、输出都是从程序运行所在内存的角度来划分的,比如,输入从硬盘到内存,称为输入流;数据从内存到硬盘,通常称为输出流。

使用原则

通常来讲,如果进行输入输出的是文本内容,就考虑用字符流;如果输入输出的内容是二进制内容,则考虑用字节流。计算机里的文件可以分为文本文件和二进制文件。虽然计算机里的文件本质上都是二进制文件,但当二进制文件里的内容能被正常解析为字符时,可以称之为文本文件。

输入/输出流的四大基类

  • 字节流:InputStreamOutputSteam
  • 字符流:ReaderWriter

计算机打开文件的时候,都会查询编码表,把底层的二进制编码转为人们熟知的字符,比如,0-127的数字会查询ACSII表,其他值查询系统默认编码表(中文就是GBK)。

InputStream/OutputSteam

FileOutputStream的常用API:

write(byte b[]); // 这个调的也是下面这个方法
write(byte b[], int off, int len) // b[]是要写入的数组,off是b[]的开始索引,len是写入的长度

FileInputStream常用 API:

int read(); // 一次读取一个字节,返回值为读取到的字节数据

// 一次读取b[]数组长度的字节,调的也是第三个的方法,数组索引从0开始,长度为b.length
// 返回值为读到缓冲区数组里的字节数量,或者-1(结束)
// 每次读完,缓冲区数组中就有了读到的字节数组
int read(byte b[]);
int read(byte b[], int off, int len);

那么,常见的应用场景就是把一个文件拷贝到另一个地方。那么步骤就是先读,再写。

// 把a文件内容拷贝到b文件
FileInputStream fis = new FileInputStream("D:\\a.txt");
FileOutputStream fos = new FileOutputStream("D:\\b.txt");
byte[] bytes = new byte[1024]; // 字节缓冲区,一般长度定义为1024或其倍数
int readLength = 0; // FileInputStream的read方法,返回值为读入缓冲区的总字节数
while ((readLength = fis.read(bytes)) != -1) {
    fos.write(bytes, 0, readLength); // 读了多少字节,就写多少字节
}
fos.close();
fis.close();

注意FileOutputStreamwrite((byte b[]))方法,它再次调用write(byte b[], int off, int len)方法,len传的缓冲区数组的长度,如果,b[]的实际内容少于它的长度,那么写入另一个文件后就会在实际内容后跟很多NULL值,这是不必要的,所以应该要用上面的写法:

fos.write(bytes, 0, readLength); // 读了多少字节,就写多少字节

Reader/Writer

用字节流读取文本文件时,会有一个问题,如果遇到中文字符是,可能不会显示完整的字符,那是因为一个中文字符可能占用多个字节存储(在 GBK 字符集下,一个中文 = 2 个字节,UTF-8 下,一个中文 = 3 个字节)。这个时候,最好用字符流来处理。

字符输出流的使用步骤跟字节输出流类似,不同点是,字符输出流的write方法,是把数据写入到内存缓冲区中(就是字符转为字节的过程),最后需要手动调用flush方法或者close方法,把数据刷新到文件中。

缓冲流

缓冲流的创建都是基于基本的流对象的

  • 字节缓冲流:BufferedInputStreamBufferedOutputStream
  • 字符缓冲流:BufferedReaderBufferedWriter

二进制流:ByteArrayStream,也很重要

字节流和字符流的区别:字符流读写会查码表,字节流不会。

flush 和 close 方法的区别

  • flush:刷新缓冲区,流对象可以继续使用;
  • close:先刷新缓冲区,然后通知系统释放资源。流对象不能再被使用了。

理解了字节流的读写过程后,字符流的也差不多。既然是字符流,肯定可以直接写charchar[]String类型的数据了。

递归

递归的分类:直接递归,间接递归

  • 直接递归:在方法中调用自身。
  • 间接递归:A 方法调 B 方法,B 方法调 A 方法。

递归注意事项:

  1. 递归一定要有条件限定,保证递归能停止,否则会发生栈内存溢出;
  2. 在递归中虽然有限定条件,但递归次数依然不能太多,否则也会发生栈内存溢出。
  3. 构造方法,禁止递归。

深拷贝、浅拷贝

👉Java深拷贝和浅拷贝

面向对象思想

一、你对面向对象思想的理解?

对象通常对应现实世界中事务的模型,定义一个对象包含至少两个部分:属性、行为。一个系统被看成是一些对象的集合;而开发一个系统,我们要确认系统中存在的对象,由此确定有哪些类(Class)。这些对象通过相互协作来完成程序任务。

二、面向对象和面向过程的区别在哪里?

在面向过程或结构化的程序中,通常情况下属性和行为是分开的。在面向对象程序中,属性和行为都包含在某一个对象中。也就是说,在面向过程的程序中,基本构成是函数;面向对象程序中,基本构成是对象。

三、举例说明面向对象思想?

四、如何进行面向对象分析?

  1. 划分职责进而识别出有哪些类。根据需求描述,我们把其中涉及的功能点,一个一个罗列出来,然后再去看哪些功能点职责相近,操作同样的属性,可否归为同一个类。
  2. 定义类及其属性和方法。我们识别出需求描述中的动词,作为候选的方法,再进一步过滤筛选出真正的方法,把功能点中涉及的名词,作为候选属性,然后同样再进行过滤筛选。
  3. 定义类与类之间的交互关系 UML。统一建模语言中定义了六种类之间的关系,它们分别是:泛化、实现、关联、聚合、组合、依赖。我们从更加贴近编程的角度,对类与类之间的关系做了调整,保留四个关系:泛化、实现、组合、依赖
  4. 将类组装起来并提供执行入口。我们要将所有的类组装在一起,提供一个执行入口。这个入口可能是一个 main() 函数,也可能是一组给外部用的 API 接口。通过这个入口,我们能触发整个代码跑起来。

编译器优化和指令重排

重排序过程:源代码 --> 1编译器优化重排序 --> 2指令级并行重排序 --> 3内存系统重排序 --> 最终执行的指令序列。

1属于编译器重排序,2,3属于处理器重排序。重排序会导致多线程程序出现内存可见性问题。

重排序-数据依赖性

编译器和处理器可能对操作做重排序。编译器和处理器在重排序时,会遵守数据依依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。

注意,这里所说的数据依赖性仅针对于单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑。

as-if-serial原则

就是编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果这些操作不存在数据依赖关系,则可能被编译器和处理器重排序。

参考资料

Java工程师成神之路

posted @   yfhu  阅读(20)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
点击右上角即可分享
微信分享提示