首页 Java基础知识
文章
取消

Java基础知识

方法

1.静态方法为什么不能调用非静态成员

  • 静态方法在类初始化时分配内存,此时对象还没初始化,也就是说还不存在this,而非静态成员需要this进行调用
  • 静态方法和静态变量是可以被子类继承的,但是会被隐藏,因为它们本身是属于类的,也就是说你调用静态成员时可以直接通过类名,既然如此你还有必要用子类来调用静态成员吗?

2.子类可以重写父类的静态方法吗:不能

  • 根据实验,向上转型的子类,调用静态方法不能体现多态,说明子类并不能重写父类的静态方法

3.equals()和==的区别

  • 对于基本数据类型,==比较的是两者的值;对于引用数据类型,==比较的是对象的内存地址
  • equals()是Object类中的实例方法,效果与==比较对象是一样的
  • 在其他类型基本重写了equals()方法,例如String类重写equals()方法后的效果是:①比较两个对象变量指向的内存变量是否相同;②判断对象是否同类型或者是父子类关系;③判断对象属性是否相同

4.为什么重写equals,需要同时重写hashCode方法

  • 有如下Java规范:obj1.equals(obj2) == true —> obj1.hashCode()==obj2.hashCode(),反之,如果obj1.hashCode()==obj2.hashCode(),obj1.equals(obj2)不一定等于true,但为了提高hash表的效率,应该使其相等
  • hashCode方法存在的意义就是为了提高hash表的查询效率,这样就可以减少equals的次数,比如HashSet和HashMap,其中HashSet底层就是利用的HashMap的key特性保证了元素的唯一性
  • 对于HashMap的put方法,对于put(key,value),首先会比较key.hashCode的值在HashMap中是否有与之相等的,如果不等直接存入,如果相等会再比较equals,判断是否为同一对象,如果确实为同一对象,那么就会覆盖原来的value,如果不为同一对象,就是存在拉链后面,该拉链的底层数据结构是链表

5.this()和super()调用的先后顺序

  • 有显式this()调用的构造器就会抑制掉该构造器里隐式的super()调用;没有显式this()调用的构造器则会得到隐式的super()调用。
  • this()调用会借助别的重载版本的构造器来做部分初始化,而一连串this()最后来到的构造器必然是没有显式this()调用的,这里就会有显式或隐式的super()调用。
  • 并不是构造子类对象的同时会构造父类对象,而是一个子类对象的实例会包含其所有基类所声明的字段,外加自己新声明的字段。那些父类声明的字段并不构成一个完整的父类的实例。super()是让父类封装对自己所声明的字段做初始化的手段。

多态

1.重写与重载的区别

  • 方法是否冲突,跟方法名和参数类型以及参数顺序有关,与返回值、方法修饰符(这里应该指的是private、protected、public)无关
  • 重载就是同一个类中多个同名方法根据不同的传参来执行不同的逻辑处理。其中返回值、修饰符、参数个数和顺序都可以不同但方法名必须相同
  • 调用哪个重载的方法是在编译期就决定了的
  • 重写就是对父类方法的改造,当子类方法中根据相同的参数需要返回不同的处理结果,就需要对方法进行重写
  • 调用重写方法是在运行起决定的
  • 重写方法的参数类型、个数和顺序必须与父类方法相同;返回值的类型如果是void或基本数据类型,则不能修改,如果是引用数据类型可以是该类型的子类;抛出的异常类型范围小于等于父类,访问修饰符的范围大于等于父类
  • 重载方法的参数列表必须修改,重写方法的参数列表不能修改

2.使用重载的方法的版本是依据调用者和方法参数,而调用者和方法参数的确定都是在编译器就确定了的;所以用了哪一个重载方法是在编译器就确定了的;相比之下,重写调用的是哪个子类的方法则是在运行期才确定的

3.分派调用

  • 方法调用过程:指确定被调用方法的版本(即调用哪一个方法),并不包括方法执行过程;

  • 分派调用的种类: 分派调用的种类

  • 静态分派:依赖于静态类型来定位方法执行版本的分派动作(如重载)称为静态分派
  • 动态分派:运行时期,依赖于实际类型来定位方法执行的分派动作(重写 Override)属于动态分派
  • 单分派与多分派:方法的接受者与方法的参数统称为方法的宗量。根据分派基于多少宗量,可以将分派划分为单分派和多分派两种

运算符

1.右移运算符»和无符号右移运算符»>

  • ex1»ex2,其中ex2表示移动的位数,ex1移动时,向右移出的位被丢弃,左边用符号位填充(是正数就用0,负数就用1)
  • ex1»>ex2,无论ex1为负数还是正数,左边统一用0填充

2.异或运算符^

类和对象

1.怎么理解Java对象:把现实中不同的数据绑定在一起形成的单一整体,称它为对象;简单来说对象就是用来存储一些关联数据的有机结合体,或者说对象可以看成是一个容器,一个集合,这个里面存放的逻辑相关的数据

2.null是所有引用类型的默认值,可以赋值给任何引用类型变量,但是不能赋值给基本类型变量;null可以转换成任何引用类型;

3.Java里float、double、long,需要在赋值直接量后面分别添加f或F、d或D、l或L尾缀来说明。不添加容易引发编译错误,丢失精度等问题

4.包装类底层的常量池技术

  • 什么是常量池,就是说事先准备了一个范围的数据放在池子(在底层叫缓存数组)里,那么基本数据类型进行自动装箱时,如果在这个范围内就会用常量池中的数据赋值,因此只要实在这个范围的基本数据类型,被装箱后指向的都是同一个对象;(注意如果是new创建的包装类对象那么就是不同的对象,所以建议在比较包装类对象时使用equals()方法)
  • 除了浮点类型float、double其他基本数据类型的包装类都实现了常量池技术
  • 常量池技术本身是用来提高性能的,试想每次包装时都需要new出对象效率是不是很低,如果在池子里事先准备好,赋值时直接指向就好了,这样就大大提高了性能,同时如果重复创建相同的数据,相当于浪费存储空间了;当然由于性能和占用空间资源的权衡,底层池子里数据的范围不会很大,不应该造成数组的长度过长,过度占用空间的情况
  • 自动装箱和拆箱:编译器自动会我们做了转换,经查看字节码,装箱其实就是调用了包装类的valueOf(),实际就调用了构造器Integer(int i)方法,拆箱其实就是调用了xxxValue(),这个方法其实就是返回final int value;所以自动装箱和拆箱也就是Java语法糖
  • String类的不可变性:对于String变量,每次重新赋值都是重新指向了一个对象而不是修改了原来的对象,这就导致String对象一经创建值就再也不会改变,因此需要大量修改字符串的操作不建议使用String;而它的不可变性究其原因是因为底层使用了final修饰的private变量存储字符串,同时没有提供任何的getter和setter;至于Java9后为什么改用byte数组存储字符串,原因是为了节省存储空间

成员变量和局部变量

1.局部变量是在代码块或方法中声明的变量,或是方法的参数

深拷贝、浅拷贝、引用拷贝

1.什么是拷贝:给定一个场景,我们需要多个相同的对象它们的各项属性都应该相同,那么我们应该采用拷贝的形式生产对象而不是new新对象,因为new新对象需要初始化操作,这样就会进行大量相同的初始化操作影响性能;而拷贝会新申请内存空间,同时保持属性值与原对象相同

2.深拷贝和浅拷贝的区别:对于内部属性是引用类型的对象来说,浅拷贝最终的复制品内部属性与原对象是同一个,而深拷贝会另外申请空间存放这个内部属性;

3.如何实现深拷贝:让内部属性在对象拷贝的同时也进行拷贝就行了;这样两者都会申请新的内存空间

4.何为引用拷贝:即多份引用变量指向同一个对象,其他引用变量对于该引用变量就是引用拷贝

抽象类和接口

1.抽象类主要用于代码复用,而接口用于限制行为,比如实现了某种接口就拥有了某个行为

2.什么叫做声明式接口:里面没有任何方法和属性,实现该接口就表示该类本身拥有某种能力;注意本身具有某种能力,也就是说并不是实现了声明式接口具有了某种能力,而是类本身就有这种能力,声明式接口只是作为一个标志,标识它由某种能力;Java种的两个声明式接口:Cloneable、Serializable;

3.如果两个接口具有相同的方法,一个类又同时实现这两个接口,对于类来说实现这一个方法就等于同时实现了这两个接口中的方法

Java反射机制

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

2.反射的主要用法

  • 创建实例
  • 反射调用方法

3.反射的应用场景

  • 松耦合,通过工厂模式+配置模式的方式替换new生成对象的方法

4.Method对象:现有一个问题:子类继承父类,并重写了父类中的方法A,那么此时在子类的Class对象中,Method数组对于A()是只有一个子类重写了的A(),还是说,父类的A()和子类的A()都在?

  • 经过源码分析:isAssignableFrom()该方法在searchMethod()方法用于判断方法返回类型是否属于父子类关系,如果returnType更小,则getMethod()会获得更小类型的那个方法,也就是说就算子类方法返回值类型与父类不同,最终调用的方法也还是子类的方法,另外,如果方法返回值相同,那么Method数组内部就只有一个子类重写的方法

5.(Class<?>)clazz.newInstance()底层默认调用的依然是无参构造器,因而我们在声明了有参构造器的同时一定要显式声明无参构造器,因为Java底层很多都是利用无参构造器生成的对象;当然我们也可以手动调用有参构造器对象的newInstance()生成对象;

注解

1.注解是什么?

  • 注解和类、接口、枚举是同一级别的
  • 注解是写给程序看的,往往出现在类、方法、成员变量、形参位置
  • 它主要为当前读取该注解的程序提供判断依据及少量附加信息;如程序读到加了@Test的方法就知道这个方法是待测试方法;又比如@Before,程序读到它就知道该方法要放在@Test方法之前执行;还有@RequestMapping(“/user/info”)提供了Controller某个接口的URL;
  • 总的来说,注解作为给程序读取的标注信息,往往利用完就会被抛弃,而这就涉及到保留策略了

2.自定义注解的三步骤

  • 定义注解
  • 在程序中(类上、方法上、域上)使用注解
  • 在其他程序读取注解信息,并执行

3.注解的保留策略

  • 注解通过保留策略,控制自己可以保留到哪个阶段。
  • 保留策略也是通过注解实现,它属于元注解,也叫元数据。具体来说是通过元数据@Retention实现的
  • 保留策略的实现有三种:SOURCE、ClASS、RUNTIME;分别对应着.java文件,.class文件,jvm的Class对象;
  • 如果在自定义注解时不加@Retention,注解的保留策略默认是CLASS
  • 使用各保留策略的注解例子:①@Override对应SOURCE,编译成.class文件就会消失
  • 通常情况我们只会使用jdk注解、第三方框架注解,,三步骤中的定义注解、读取执行两个步骤用不着我们来做,都被jdk和框架封装隐藏了,这其实也导致了大部分人对注解的陌生

异常

1.不受检查异常,在Java编译过程当中,如果不受检查异常不以catch/throw处理就没办法通过编译

2.并不是说finally子句一定会执行,finally子句不会执行的情况如下:
①在finally之前,jvm被终止运行(System.exit(1))
②程序所在线程死亡
③关闭CPU

2.捕获异常

  • try-with-resource,jdk9之前:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    // 1.将配置文件加载进内存(自动关闭InputStream)
try (InputStream inputStream = JdbcUtil.class.getResourceAsStream("/jdbc.properties")) {

      // 2.获取Properties参数
  Properties prop = new Properties();
  prop.load(inputStream);

      // 3.配置数据源
  DruidDataSource dataSource = (DruidDataSource) DruidDataSourceFactory.createDataSource(prop);

      // 4.配置JdbcTemplate
  jdbcTemplate = new JdbcTemplate(dataSource);

} catch (Exception e) {
  e.printStackTrace();
}

jdk9版本之后,不用在try语句块中声明变量了,直接在里面写上引用变量,多个变量之间用分号分隔就ok了

  • try-with-resource的出现也是为了解决finally语句的冗余,因为这些语句与业务代码没什么关系,放在代码里可读性会变得很差;

泛型

1.泛型是一种编译期的代码模板技术,具有存入时类型检查和取出时自动转型的优点。尤其要注意编译器

2.泛型的的出现是为了解决什么问题:以ArrayList为例,它的底层数组是Object数组类型的,也就是说当往Object数组中存入数据并且取出时就会产生两个问题:

  • 需要强制类型转换
  • 强制类型转换容易产生问题(比如如果有人在不经意的情况下传入了int类型的数据,而取出时强制转换成String,这样就会报错)

3.泛型类型擦除与自动类型转换

  • 仍然以ArrayList<T>为例,虽然这个时候ArrayList是个泛型类,并且我们传入类型参数User,但是ArrayList底层数组其实还是Object数组类型的;但是这与我们实际情况有点矛盾:传入不合适的数据编译器会报错;取出数据我们不需要类型强制转换;其实这些事情都由编译器自动帮我们完成了;
  • 泛型是JDK专门为编译器创造的语法糖,只在编译期,由编译器负责解析,虚拟机不知情
  • 存入:普通类继承泛型类并给变量类型T赋值后,就能强制让编译器帮忙进行类型校验
  • 取出:代码编译时,编译器底层会根据实际类型参数自动进行类型转换,无需程序员在外部手动强转
  • ArrayList<T>在实际被编译后泛型参数就已经被擦除了,最终类型就是ArrayList;所以实际上根本不存在ArrayList<User>类型等泛型类型

4.List<? extends Number> list对于传入的list只能做读操作,不能做写操作。

5.泛型边界

  • 泛型边界是为了让泛型操作模板化,一般会作为参数接收其他的泛型类如果想要充分理解泛型边界那么理解这句话尤其重要

  • ArrayList<? extends Integer>,ArrayList<? super Integer>,前者add操作是不被允许的,后者可以存入Integer和它的子类;当我们考虑存入操作时往往要考虑取出操作,同时要明确引用具体的指向;例如前者它可以指向ArrayList<?>,这个?是Integer的子类,也就是说同一时刻ArrayList<? extends Integer>它只能指向一个,但是这些子类相互可能并没有继承关系,但实现强制转换时就容易出错,而后者之所以可以存入,是因为它指向的实际对象它们都有公共子类也就是Integer,也就是说只要存入的是Integer或它的子类就能正常向上转型

  • 如果存入操作比较多,推荐使用ArrayList<? super Integer>,取出操作比较多推荐使用ArrayList<? extends Integer>,因为它接收的类型是Integer,而不是Object,相较而言更加精确,能使用的方法会更多

6.泛型类和泛型方法常见使用场景:封装统一结果类和工具方法时使用比较频繁

7.静态方法不能使用泛型类的类型参数,因为类型参数的指定通常是在创建泛型类的时候;

5.什么叫做Java语法糖

语法糖(Syntactic Sugar),也称糖衣语法,是由英国计算机学家Peter.J.Landin发明的一个术语,指在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。Java 中最常用的语法糖主要有泛型、变长参数、条件编译、自动拆装箱、内部类等。虚拟机并不支持这些语法,它们在编译阶段就被还原回了简单的基础语法结构,这个过程成为解语法糖。

JDK动态代理

  • 动态代理的使用场景

多个实现了某个接口的类们,这些接口方法都需要调用一类通用方法,这样每个实现类就需要在方法内部调用这个方法,这样确实实现的代码复用,但是(按照内联方法的思想,方法经调用之后就被替换成方法体,也就是说实际上本身对这些方法的调用是不是就是冗余了)由于每个实现类对于这个方法都要调用同一段代码,这本身似乎就是代码冗余,有没有一种方法可以让各个实现类在实现方法时只关心它本身的业务而不用关心通用的那部分,这就需要用到JDK动态代理,利用调用处理器的invoke()规定好通用方法与实现方法的关系,然后创建实现类的代理对象,实现调用方法时,调用的就是代理对象的invoke方法

  • 使用动态代理的流程,如下图:

动态代理流程图

静态代理

  • 静态代理的实现流程,如下图: 静态代理

静态代理和JDK动态代理的区别

  • JDK动态代理不用可以编写代理类,由jdk为我们提供的Proxy作为代理类,在动态代理中我们只在意代理对象和增强代码,因而代理类对我们来说是多余的,我们只要能获取到代理对象就OK了
  • JDK动态代理的增强代码不与代理类耦合
  • 代理就是俄罗斯套娃,层层封装,层层屏蔽

序列化和反序列化

1.通过ObjectOutputStream和ObjectInputStream对对象进行序列化及反序列化

2.被transient关键字修饰的属性值不会被保存进序列化文件,故反序列化后的属性值是变量类型的默认值。

3.虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(private static final long serialVersionUID)

4.static 变量因为不属于任何对象(Object),所以无论有没有 transient 关键字修饰,均不会被序列化。

5.在SpringMVC中用@RequestBody将json对象转换成java对象是反序列化吗?

高内聚低耦合

1.松耦合的目的

  • 某一模块更改下,不会影响到其他模块;模块之家更加独立,这样在维护代码时就不会产生该一处代码,带动其他模块大量的代码改动

匿名内部类

1.在方法内部定义的匿名内部类(局部匿名类)如何使用方法中定义的局部变量,包括基本数据类型、字符字面量、引用变量

源代码: 匿名内部类原始文件

反编译结果: 匿名内部类反编译

1.可见局部匿名类使用外部基本数据类型或者字符串常量时会直接用字面量替换,而使用引用类型变量时会在内部定义相同的有final修饰的属性,并通过构造器用外部引用变量初始化,保证二者操作同一个对象(语义相同)

2.如果局部变量被内部类引用,要么变量用final修饰,要么就不在内部修改(最终还是保证:内部类的final变量与外部变量操作的是同一个对象),否则会报编译错误(另外final修饰的基本数据类型变量和字符字面量赋值的变量,经过编译后不会使用变量(或许是jvm中提到的字符引用),而会直接用字面量替换(或许是jvm中提到的直接引用))

如果外部变量不用final修饰

函数式接口

1.Java内置四大核心函数式接口,java.util.function包下定义了各式各样的函数式接口

2.如何理解函数式接口

四大核心函数式接口

lambda表达式

1.为什么Comparator可以使用lambda表达式,它明明由两个抽象方法:

Comparator接口的两个抽象方法

如下:

为什么Comparator接口有两个抽象方法

总的来说:如果一个接口没有直接父接口,就会隐式声明一个与Object公共方法(非final)相同(或者说符合子类覆盖原则的方法);当然如果直接显示声明了一个Object中的方法,就不是再隐式声明一个了。注意接口在被实现类实现时这个方法会自动实现

2.函数式接口只能有一个抽象方法,而对于匿名类的实现,接口中可以有多个抽象方法,只需要再匿名类中全部实现就可以了

3.lambda表达式与匿名类的区别

下面看看lambda表达式的反编译结果:

原始lambda:

原始lambda

反编译lambda:

反编译lambda

可见lambda和匿名类的实现并不相同,lambda并不是匿名类的语法糖。

2.lambda的本质

lambda的本质

3.lambda的独特性

lambda可以让我们决定方法的实现,也就是说实现了方法的多态,这是Java函数式编程与其他脚本语言函数式编程的区别

本文由作者按照 CC BY 4.0 进行授权