Java基础知识第一讲:基础语法(下)

简介: Java基础知识第一讲:基础语法
3.2、数组的使用

一维数组的使用:初始化

  • 动态初始化: 数组声明且为数组元素分配空间与赋值的操作分开进行
int[] arr = new int[3];
arr[0] = 3;
arr[1] = 9;
arr[2] = 8;
  • 静态初始化: 在定义数组的同时就为数组元素分配空间并赋值
int[] arr = {3,9,8};

每个数组都有一个属性length指明它的长度,例如: a.length 指明数组a的长度(元素个数)

内存结构

创建基本数据类型数组

图1

图2

图3


4、对泛型编程的了解?

4.1、泛型的定义
  • 泛型的本质是参数化类型,泛型提供了编译时类型的安全检测机制,该机制允许程序在编译时检测非法的类型,比如要实现一个能够对字符串(String)、整形(Int)、浮点型(Float)、对象(Object)进行大小比较的方法,就可以使用Java泛型
  • 在不使用泛型的情况下,我们可以通过引用Object类型来实现参数的任意化,因为在Java中Object类是所有类的父类,但在具体使用时需要进行强制类型转换。强制类型转换要求开发者必须明确知道实际参数的引用类型,不然可能引起前置类型转换错误,在编译期无法识别这种错误,只能在运行期检测这种错误(即只有在程序运行出错时才能发现该错误)。而使用泛型的好处是在编译期就能够检查;
  • 意味着编写的代码可以被不同类型的对象所重用。
  • 我们提供了泛指的概念,但具体执行的时候却可以有具体的规则来约束,比如我们用的非常多的ArrayList就是个泛型类,ArrayList作为集合可以存放各种元素,如Integer, String,自定义的各种类型等,但在我们使用的时候通过具体的规则来约束,如我们可以约束集合中只存放Integer类型的元素。
    List iniData = new ArrayList<>()

泛型是jdk5.0版本出来的新特性,主要有两个好处:

  • 第一,是提高了数据类型的安全性,可以将运行时异常提高到编译时期;比如ArrayList类就是一个支持泛型的类,这样我们给ArrayList声明成什么泛型,那么他只能添加什么类型的数据;
  • 第二,也是我个人认为意义远远大于第一个的. 就是他实现了对代码的抽取:大大简化了代码的抽取,提高了开发效率。
4.2、泛型标记和泛型限定:E、T、K、V、N、?

在使用泛型前首先要了解有哪些泛型标记

类型通配符使用“? ”表示所有具体的参数类型,例如List<? >在逻辑上是List、List等所有List<具体类型实参>的父类。在使用泛型的时候,若希望将类的继承关系加入泛型应用中,就需要对泛型做限定,具体的泛型限定有对泛型上线的限定和对泛型下线的限定;

1.对泛型上限的限定:<? extends T>

  • 在Java中使用通配符“? ”和“extends”关键字指定泛型的上限,具体用法为<? extends T>,它表示该通配符所代表的类型是T类的子类或者接口T的子接口。

2.对泛型下限的限定:<? super T>

  • 在Java中使用通配符“? ”和“super”关键字指定泛型的下限,具体用法为<? super T>,它表示该通配符所代表的类型是T类型的父类或者父接口。
4.3、泛型方法

泛型方法指将方法的参数类型定义为泛型,以便在调用时接收不同类型的参数。在方法的内部根据传递给泛型方法的不同参数类型执行不同的处理方法,具体用法如下

以上代码通过public static < T > void generalMethod( T … inputArray )定义了一个泛型方法,该方法根据传入数据的不同类型执行不同的数据处理逻辑,然后通过generalMethod(“1”,2, newWroker())调用该泛型方法。注意,这里的第1个参数是String类型,第2个参数是Integer类型,第3个参数是Wroker类型(这里的Wroker是笔者自定义的一个类),程序会根据不同的类型做不同的处理。

4.4、泛型类

泛型类指在定义类时在类上定义了泛型,以便类在使用时可以根据传入的不同参数类型实例化不同的对象。

泛型类的具体使用方法是在类的名称后面添加一个或多个类型参数的声明部分,在多个泛型参数之间用逗号隔开。具体用法如下

比如我们对数据的操作,如果我们有Person、Department、Device三个实体,每个实体都对应数据库中的一张表,每个实体都有增删改查方法,这些方法基本都是通用的,因此我们可以抽取出一个BaseDao,里面提供CRUD方法,这样我们操作谁只需要将我之前提到的三个类作为泛型值传递进去就OK了(被广泛应用)

Demo1,在模板模式代码里面,定义模板时,使用泛型

  • 在以下代码中通过public class AbstractItemWriteService定义了一个泛型类,可根据不同的需求参数化不同的类型(参数化类型指编译器可以自动定制作用于特定类型的类)
// 抽象父类
public abstract class AbstractItemWriteService<T extends BaseInput> {
    private static final int PAGE_NO = 1;
    private static final int PAGE_SIZE = 100;
    // 定义通用模板方法
    public abstract Object itemWriteTemplate(T param);
}
// 子类1 实现类
public class UpdateItemStatusByDistrictCodeImpl extends AbstractItemWriteService<ItemStatusParam> {
    public final OpenResponse<List<DistrictItemDto>> itemWriteTemplate(ItemStatusParam param) {
          OpenItemWriteContext itemWriteContext = new OpenItemWriteContext();               
          itemWriteContext.setParamType(ItemWriteBizTypeEnum.UPDATE_ITEM_STATUS);
          // 1、前置处理  todo 去掉返回值
          preHandleItemWrite(param, itemWriteContext);
          // 2、业务逻辑
          return handlerItemWrite(param, itemWriteContext);
    }
}
// 子类2 实现类
4.5、泛型接口

泛型接口的声明和泛型类的声明类似,通过在接口名后面添加类型参数的声明部分来实现。泛型接口的具体类型一般在实现类中进行声明,不同类型的实现类处理不同的业务逻辑。具体的实现代码如下

以下代码通过public interface IGeneral定义了一个泛型接口,并通过public classGeneralIntergerImpl implements IGeneral定义了一个Integer类型的实现类

4.6、类型擦除

在编码阶段采用泛型时加上的类型参数,会被编译器在编译时去掉,这个过程就被称为类型擦除。因此,泛型主要用于编译阶段。在编译后生成的Java字节代码文件中不包含泛型中的类型信息。例如,编码时定义的List和List在经过编译后统一为List。JVM所读取的只是List,由泛型附加的类型信息对JVM来说是不可见的。

Java类型的擦除过程为:首先,查找用来替换类型参数的具体类(该具体类一般为Object),如果指定了类型参数的上界,则以该上界作为替换时的具体类;然后,把代码中的类型参数都替换为具体的类。


5、面向对象的核心逻辑

5.1、Java的四个基本特性(抽象、封装、继承、多态),对多态的理解(多态的实现方式)以及在项目中的那些地方用到了多态? 阿里面经第三题
  • 1、Java面向对象的四个特性
  • 封装:为封装是把数据和操作数据的方法绑定起来,对数据的访问只能通过已定义的接口 。面向对象的本质就是将现实世界描绘成一系列完全自治、封闭的对象。我们在类中编写的方法就是对实现细节的一种封装;我们编写一个类就是对数据和数据操作的封装。可以说:封装就是隐藏一切可隐藏的东西,只向外界提供最简单的编程接口
  • 抽象:抽象是将一类对象的共同特性 总结出来构造类的过程,包括数据抽象和行为抽象两方面,抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。
  • 继承继承是从已有类得到继承信息创建新类的过程。继承让变化中的软件系统有了一定的延续性,同时继承也是封装程序中可变因素的重要手段。
  • 多态允许不同子类型的对象对同一消息作出不同的响应
  • 2、对多态的理解(多态的实现方式)
  • 多态性分为编译时的多态性和运行时的多态性。
    方法重载(overload)实现的是编译时的多态性(仅仅返回值不同,不算方法重载,编译出错)
    方法重写(override)实现的是运行时的多态性
    实现多态需要做两件事:1、方法重写(子类继承父类并重写父类中已有的或抽象的方法); 2、对象造型(用父类型引用 引用子类型对象,这样同样的引用调用同样的方法就会根据子类对象的不同而表现出不同的行为
  • 3、项目中对多态的应用
  • 工作台系统中,有两种校验类型,按公务卡校验和按手机号校验。他们有相同的方法CheckInfo(), 但是校验的数据是不同的,一个来源于用户表,一个来源于公务卡系统,也就是校验时会有不同的操作,两种校验操作都继承父类的checkInfo()方法,但对于不同对象,拥有不同的操作。 20210712 刚做的项目。
5.2、面向对象和面向过程的区别?用面向过程可以实现面向对象吗?那是不是不能面向对象?

1、面向对象和面向过程的区别?

  • 面向过程就像是一个细心的管家,事无巨细的都要考虑到。而面向对象就像是家用电器,你字需要知道他的功能,不需要知道它的工作原理;
  • “面向过程”是一种以事件为中心的编程思想。就是分析出解决问题所需的步骤,然后用函数把这些步骤实现,并按顺序调用。面向对象是以“对象”为中心的编程思想。
  • 举个例子:汽车发动、汽车到站
  • 这对于“面向过程”来说,是两个事件,汽车启动是一个事件,汽车到站是另一个事件,面向过程编程的过程中我们关心的是事件,而不是汽车本身。针对上述两个事件,形成两个函数,之后依次调用;
  • 然而对于面向对象来说,我们关注的是汽车这类对象,两个事件只是这类对象所具有的行为。而且对于这两个行为的顺序没有强制要求。

2、用面向过程可以实现面向对象吗?那是不是不能面向对象?

5.3、重载和重写,如何确定调用哪个函数? 阿里面经第5题
  • 重载: 重载发生在同一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者两者都不同)则视为重载;
  • 重写: 重写发生在子类和父类之间,重写要求子类被重写方法与父类被重写方法有相同的返回类型,比父类被重写更好访问,不能比父类被重写方法声明更多的异常(里氏替换原则)。根据不同的子类对象确定调用哪个方法。
5.4、接口与抽象类的区别?

相同点:不能被实例化,子类只有实现类接口的方法,或抽象类的方法,才能被实例化

区别: 接口只有定义,方法不能在接口中实现(java8之后,接口也是可以有方法实现的,函数式编程就是只有一个抽象方法的接口),抽象类可以有被实现的方法

接口用implements实现,接口可以多实现 抽象类被extends继承 ,单继承

接口强调对特定功能的实现 常用的功能;抽象类强调所属关系(父子)充当公共类

接口成员方法public 成员变量默认 public static final 赋初值,不能修改 抽象类成员变量default方法(本包可见),抽象方法abstract (不能用private,static,synchronized,native)分号结尾

使用时机:当想要支持多重继承,或是为了定义一种类型请使用接口;当打算提供带有部分实现的“模板”类,而将一些功能需要延迟实现请使用抽象类;当你打算提供完整的具体实现请使用类

5.5、面向对象编程的六个基本设计原则(单一职责、开放封闭、里氏转换、依赖反转、合成聚合复用、接口隔离),迪米特法则,在项目中用过哪些原则 在王争的《设计模式之美》中讲解的很到位

六个基本原则:

单一职责:一个类只做它该做的事情(高内聚)。在面向对象中,如果只让一个类完成它该做的事,而不涉及与它无关的领域就是践行了高内聚的原则,这个类就只有单一职责。

  • 类和对象最好只有单一职责,若是承担多种义务,可以考虑进行拆分

开关原则软件实体应当对扩展开放,对修改关闭,避免因为新增同类功能而修改已有实现。要做到开闭有两个要点:①抽象是关键,一个系统中如果没有抽象类或接口,系统就没有扩展点; ②封装可变性,将系统中的各种可变因素封装到一个继承结构中,如果多个可变因素混杂在一起,系统将变得复杂而混乱。

里氏替换:进行继承关系抽象时,凡是可以用父类或基类的地方,都可以用子类替换父类型。子类一定是增加父类的能力而不是减少父类的能力。

依赖反转:实体应该依赖抽象而不是实现,面向接口编程(该原则说的直白点具体点就是声明方法的参数类型、方法返回类型、变量的引用类型时,尽可能使用抽象类型而不用具体类型,因为抽象类型可以被它的任何一个子类型所替代)

合成聚合复用:优先使用聚合或合成关系复用代码。

接口分离:不要在一个接口与中定义太多的方法,可以将其拆分成功能单一的多个接口,将行为进行解耦。

迪米特法则

  • 迪米特法则又叫最小知识原则,一个对象应当对其他对象有应可能少的了解。

项目中用到的原则

  • 单一职责、开关原则、合成聚合复用(最简单的例子是String类)、接口隔离。

案例:原来的代码:

public class VIPCenter {
     void serviceVIP(T extend User user>) {
         if (user insanceof SlumDogVIP) {
            // 穷X VIP,活动抢的那种
            // do somthing
         } else if(user insanceof RealVIP) {
            // do somthing
         }
         // ...
     }
}

利用开关原则(对拓展开放,对修改关闭),我们可以尝试改造为下面的代码:

interface ServiceProvider{
    void service(T extend User user) ;
}
class SlumDogVIPServiceProvider implements ServiceProvider{
    void service(T extend User user){
        // do somthing
    }
}
class RealVIPServiceProvider implements ServiceProvider{
    void service(T extend User user) {
    // do something
    }
}
public class VIPCenter {
     private Map<User.TYPE, ServiceProvider> providers;
     void serviceVIP(T extend User user) {
        providers.get(user.getType()).service(user);
     }
}
5.6 、创建一个类的实例都有哪些办法?

new 工厂模式是对这种方式的包装

clone 克隆一个实例

forclass()然后newInstance() java的反射 反射使用实例:Spring的依赖注入、切面编程中动态代理

实现序列化接口的类,通过IO流反序列化读取一个类,获得实例

  • 2、访问权限?
    private 当前类
    default 同包
    protected 子类
    public 其他类

5.12、继承和组合的区别和应用场景 20210702补
java开发技巧 优点 缺点
继承 1、支持扩展,通过继承父类实现,但会使系统结构较复杂,2、易于修改被复用的代码 1、代码白盒复用,父类的实现细节暴露给子类,破坏了封装性;2、当父类的实现代码修改时,可能使得子类也不得不修改,增加维护难度。3、子类缺乏独立性,依赖于父类,耦合度较高 4、不支持动态拓展,在编译期就决定了父类
组合 1、代码黑盒复用,被包括的对象内部实现细节对外不可见,封装性好。2、整体类与局部类之间松耦合,相互独立。3、支持扩展 4、每个类只专注于一项任务 5、支持动态扩展,可在运行时根据具体对象选择不同类型的组合对象(扩展性比继承好) 创建整体类对象时,需要创建所有局部类对象。导致系统对象很多。
  • 结论与使用建议:组合的优点明显多于继承,再加上java中仅支持单继承,所以:
    除非两个类之间是is-a的关系,否则尽量使用组合。

5.13、java接口和抽象类的区别,什么时候该用接口什么时候该用抽象类 20210702补

java接口和抽象类的区别,什么时候该用接口什么时候该用抽象类

5.14、Object类中有哪些方法?3个常用,5个线程相关

Object类是Java中其他所有类的祖先,基类

构造方法,

toString(), //toString()方法返回该对象的字符串表示

equals, //比较对象(内存地址)是否相同

hashCode, //返回一个整形数值,表示该对象的哈希值

getClass, //final方法,获得运行时类(class对象)

finalize, //当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法

clone, //实现对象的浅复制,需实现了Cloneable接口;(可重写实现字段深复制)

三个wait(), //调用此方法所在的当前线程等待,直到在其他线程上调用对象的notify(),notifyAll()方法

wait(long timeout) //线程等待,直到notify() notifyAll() 方法,或超过指定的时间量

wait(long timeout, int nanos)//线程等待,notify() notifyAll() 方法,或其他某个线程中断当前线程,或超过时间量

notify, //唤醒在此对象监视器上等待的单个(所有)线程

notifyAll. /方法调用后,线程不会立即释放所持有的锁,直到其所在同步代码块中的代码执行完毕,再释放锁


5.15、你知道Java的继承机制吗?为什么这么做?

单继承多实现

第一方面:

如果一个类继承了类A和类B,A和B都有一个C方法,那么当我们用这个子类对象调用C方法的时候,

jvm就晕了,因为他不能确定你到底是调用A类的C方法还是调用了B类的C方法。

假设A和B都是接口,都有C方法,那么问题就能解决了,因为接口里的方法仅仅是个方法的声明,

并没有实现,子类实现了A和B接口只需要实现一个C方法就OK

第二方面:

Java是严格的面向对象思想的语言,一个孩子只能有一个亲爸爸


5.16、为什么函数不能根据返回类型来区分重载?华为面试

因为调用时不能指定类型信息,编译器不知道你要调用哪个函数

1.float max(int a, int b);

2.int max(int a, int b);

当调用max(1, 2);时无法确定调用的是哪个,单从这一点上来说,仅返回值类型不同的重载是不应该允许的。


5.17、char型变量中能不能存储一个中文汉字,为什么?

char类型可以存储一个中文汉字,因为Java中使用的编码是Unicode,一个char类型占 2 个字节(16 比特),所以放一个中文是没问题的

补充:使用Unicode意味着字符在JVM内部和外部有不同的表现形式,在JVM内部都是Unicode,当这个字符被从JVM内部转移到外部时(例如存入文件系统中),需要进行编码转换。所以 Java 中有字节流和字符流,以及在字符流和字节流之间进行转换的转换流,如InputStreamReader和OutputStreamReader,这两个类是字节流和字符流之间的适配器类,承担了编码转换的任务;


5.18、常用API?

1、Math.round(11.5)等于多少? 12 Math.round(- 11.5) 又等于多少? -11

2、switch 是否能作用在byte上,是否能作用在 long 上,是否能作用在 String上?

long不行,其他都可以(string在java7开始,可以)

3、什么是Java Timer类?如何创建一个有特定时间间隔的任务?

Timer是一个调度器,可以用于安排一个任务在未来的某个特定时间执行或周期性执行,

TimerTask是一个实现了Runnable接口的抽象类,我们需要去继承这个类来创建我们自己的定时任务并使用Timer去安排它的执行

Timer timer = new Timer();
 timer.schedule(new TimerTask() {
         public void run() {
             System.out.println("abc");
         }
 }, 200000 , 1000);

5.19、请说出下面程序的输出?
class StringEqualTest {
   public static void main(String[] args) { 
           String s1 = "Programming"; 
           String s2 = new String("Programming");
           String s3 = "Program"; 
           String s4 = "ming"; 
           String s5 = "Program" + "ming"; 
           String s6 = s3 + s4; 
           System.out.println(s1 == s2);               //false 
           System.out.println(s1 == s5);               //true 
           System.out.println(s1 == s6);              //false 
           System.out.println(s1 == s6.intern());   //true 
           System.out.println(s2 == s2.intern());    //false 
}

两个知识点:

  • 1.String对象的intern()方法会得到字符串对象在常量池中对应的版本的引用;如果常量池中没有对应的字符串,则该字符串将被添加到常量池中,然后返回常量池中字符串的引用;但是,通过new方法创建的String对象是不检查字符串池的,而是直接在堆区或栈区创建一个新的对象,也不会把对象放入池中

例子:String s = new String(“abc”);创建了几个 String Object?

2个 abc pool中常量池中 new String(“abc”)堆中

student s = new student() 在内存中做了哪些事?

加载student.class文件进内存

在栈内存为s开辟空间;

在堆内存为new student()开辟空间;

对学生对象的成员变量进行默认初始化;

对学生对象的成员变量进行默认初始化;

通过构造方法对学生对象的成员变量赋值;

学生对象初始化完毕,把对象地址赋值给s变量。

  • 2.字符串的+操作其本质是创建了StringBuilder 对象进行 append 操作,然后将拼接后的 StringBuilder 对象用 toString 方法处理成 String 对象,
    这一点可以用 javap -c StringEqualTest.class 命令获得 class 文件对应的 JVM 字节码指令就可以看出来。

5.20、int和Integer有什么区别?谈谈Integer的值缓存范围***(提醒:越是貌似简单的面试题其中的玄机就越多,需要面试者有相当深厚的功力。)
  • 1、理解自动装箱和拆箱?
    Java平台为我们自动进行了一些转换,保证不同的写法在运行时等价,它们发生在编译阶段,也就是生成的字节码是一致的
    装箱:javac替我们自动把整数装箱转换为Integer.valueOf()
    拆箱:拆箱替换为Integer.intValue()
  • 2、(自动装箱和拆箱 java5引入)下面Integer类型的数值比较输出的结果为?
    integer f1=100,f2=100,f3=150,f4=150;
    新增静态工厂方法valueOf,在调用它的时候会利用一个缓存机制,如果整型字面量的值在-128到127之间,那么不会new新的Integer对象,而是直接引用常量池
    中的Integer对象,所以上面的面试题中f1f2的结果是true,而f3f4的结果是false
    bealean;缓存true/false对应的实例,只返回常量实例Boolean.TRUE/FALSE
    short:缓存-128/127之间的数值
    byte:全部缓存
    character:缓存范围‘\u0000’到‘\007F’
    以上包装类型都被声明为private final //都是不可变类型
  • 3、为什么我们需要原始数据类型,Java的对象似乎也很高效,应用中具体会产生哪些差异?
    使用:建议避免无意中的装箱、拆箱行为
    使用原始数据类型、数组甚至本地代码实现/替换掉包装类、动态数组,在性能极度敏感的场景往往具有比较大的优势
    下面是一个常见的线程安全计数器实现
class Counter {
     private fnal AtomicLong counter = new AtomicLong();
     public void increase() {
         counter.incrementAndGet();
     }
 }

如果利用原始数据类型,可以将其修改为

class CompactCounter {
    private volatile long counter;
    private satic fnal AtomicLongFieldUpdater<CompactCounter> updater = AtomicLongFieldUpdater.newUpdater(CompactCounter.class, "counter");
    public void increase() {
        updater.incrementAndGet(this);
    }
}
  • 4、阅读过Integer源码吗?分析下类或某些方法的设计要点。
    1、继续深挖缓存,Integer的缓存范围虽然默认是-128到127,但是在特别的应用场景,比如我们明确知道应用会频繁使用更大的数值,这时候应该怎么办呢?
    缓存上限值实际是可以根据需要调整的,JVM提供了参数设置:-XX:AutoBoxCacheMax=N
    String integerCacheHighPropValue = VM.getSavedProperty(“java.lang.Integer.IntegerCache.high”);
    2、引用类型局限性
    我们知道Java的对象都是引用类型,如果是一个原始数据类型数组,它在内存里是一段连续的内存,而对象数组则不然,数据存储的是引用,对象往往是分散地存储在堆的不同位
    置。这种设计虽然带来了极大灵活性,但是也导致了数据操作的低效,尤其是无法充分利用现代CPU缓存机制
  • 5、java中的integerCache
    Java5为了减少大量创建Integer的开销、提高性能,采用享元模式,引入Integer实例缓存,将-128至127范围数值的Integer实例缓存在内存中,
    这里的缓存就是使用Integer中的辅助性的私有IntegerCache静态类实现。
    不仅是Integer,Short、Byte、Long、Char都具有相应的Cache。但是Byte、Short、Long的Cache有固定范围:-128至127;Char的Cache:0至127
    这里容易出选择题
  • int和Integer的区别?20181114 选择题常考
    1、Integer是int的包装类,int是基本类型
    2、Integer变量必须实例化后才能使用,而int变量不需要
    3、Integer实际是对象的引用,当new一个Integer时,实际上生成一个指针指向此对象;而int则是直接存储数据值
    4、Integer的默认值是null,int默认值是0;
    延伸:
    1、两个通过new生成的Integer变量永远是不相等的
Integer i = new Integer(100);
 Integer j = new Integer(100);
 System.out.print(i == j); //false

2、Integer变量和int变量比较时,只要两个变量的值是相等的,则结果为true

//因为包装类integer和基本数据类型int比较时,java会自动拆包为int,然后进行比较

Integer i = new Integer(100);
int j = 100;
System.out.print(i == j); //true

3、非new生成的Integer变量和new Integer()生成的变量比较时,结果为false

//因为非new生成的Integer变量指向的是java常量池中的对象;new Integer()生成的变量指向堆中新建的对象

Integer i = new Integer(100);
Integer j = 100;
System.out.print(i == j); //false

4、对于两个非new生成的Integer对象,进行比较时,如果两个变量的值在区间-128到127之间,则比较结果为true,如果两个变量的值不在此区间,则比较结果为false

Integer i = 100;                           Integer i = 128;
 Integer j = 100;                        Integer j = 128;
 System.out.print(i == j); //true        System.out.print(i == j); //false

原因:java在编译integer i= 100时,会翻译成integer i = Integer.valueof(100); 而javaAPI对integer类型的valueof定义如下:

java对于-128到127之间的数,会进行缓存,Integer i=127时,会将127进行缓存,下次再写Integer j=127时,就会直接从缓存中取,就不会new了。

6、你知道对象的内存结构是什么样的吗?比如,对象头的结构。如何计算或者获取某个Java对象的大小?

来自深入理解jvm

象由三部分组成,对象头,对象实例,对齐填充


5.21、如何实现对象克隆?

有两种方式。

1、实现Cloneable接口并重写 Object 类中的 clone()方法;

2、实现Serializable接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆。

基于序列化和反序列化实现的克隆不仅仅是深度克隆,更重要的是通过泛型限定,可以检查出要克隆的对

象是否支持序列化,这项检查是编译器完成的,不是在运行时抛出异常,这种是方案明显优于使用Object 类的 clone

方法克隆对象。让问题在编译的时候暴露出来总是好过把问题留到运行时

5.22、String/StringBuffer/StringBuilder的区别,扩展再问他们的实现?

1、String/StringBuffer/StringBuilder的区别

String 不可变类 值不能被修改 初始化时,能用构造函数,也能赋值

字符串操作不当可能会产生大量临时字符串

String的特性

1、不可变:是指String对象一旦生成,则不能再对它进行改变。不可变的主要作用在于当一个对象需要被多线程共享,并且访问频繁时,可以省略同步和锁等待的时间,从而大幅度提高系统

性能。不可变模式是一个可以提高多线程程序的性能,降低多线程程序复杂度的设计模式

2、针对常量池的优化。当2个String对象拥有相同的值时,他们只引用常量池中的同一个拷贝

string应用场景

字符串内容不经常发生变化的业务场景:常量声明、少量的字符串拼接操作

StringBuffer 可变类 可改变值 只能用构造函数 线程安全(把各种修改数据的方法都加上了synchronized关键字)

我们可以用append或add方法,把字符串添加到已有序列的末尾或指定位置

StringBuffer应用场景:频繁进行字符串的运算(如拼接、替换、删除等),并且运行在多线程环境下,建议使用StringBufer,例如XML解析,HTTP参数解析与封装

StringBuilder(推荐) 可被修改的字符串 线程不安全

为了实现修改字符序列的目的,StringBufer和StringBuilder底层都是利用可修改的(char,JDK 9以后是byte)数组,二者都继承了AbstractStringBuilder,里面包含了基本

操作,区别仅在于最终的方法是否加了synchronized

StringBuilder应用场景:频繁进行字符串的运算(如拼接、替换、删除等),并且运行在单线程环境下,建议使用StringBuilder,例如SQL语句拼装、JSON封装等

2、扩容操作细节:

构建时初始字符串长度加16(如果没有构建对象时输入最初的字符串,那么初始值就是16)

扩容会产生多重开销,因为要抛弃原有数组,创建新的(可以简单认为是倍数)数组,还要进行arraycopy

3、字符串拼接的例子:String myStr = “aa” + “bb” + “cc” + “dd”;?(华为)

在JDK8中,字符串拼接操作会自动被javac转换为StringBuilder操作

JDK9里面则是因为Java9为了更加统一字符串操作优化,提供了StringConcatFactory,作为一个统一的入口

什么情况下用“+”运算符进行字符串连接比调用 StringBuffer/StringBuilder 对象的append方法连接字符串性能更好? 华为

String s = "abc";           
String ss = "ok" + s + "xyz" + 5;
反编译
String ss = (new StringBuilder("ok")).append(s).append("xyz").append(5).toString();

在Java中无论使用何种方式进行字符串连接,实际上都使用的是 StringBuilder。

在for循环中,尽量使用stringbuilder不使用“+”

4、字符串缓存(字符串常量池)

方案1:(-XX:+PrintStringTableStatisics)

String在Java 6以后提供了intern()方法,目的是提示JVM把相应字符串缓存起来,以备重复使用。在我们创建字符串对象并调用intern()方法的时候,如果已经有缓存的字符串,

就会返回缓存里的实例,否则将其缓存起来。(由于jdk6被缓存的字符串存在PermGen里,空间有限,使用不当就会OOM,后续版本中,缓存被放置到堆中,JDK8中永久代被MetaSpace元数据区替代)

方案2:(默认关闭 -XX:+UseStringDeduplication)

Oracle JDK 8u20之后,推出了一个新的特性,也就是G1 GC下的字符串排重。它是通过将相同数据的字符串指向同一份数据来做到的,是JVM底层的改变,并不需要Java类库做什么修改

5、String自身的演化

Java的字符串,在历史版本中,它是使用char数组来存数据的,这样非常直接。但是Java中的char是两个bytes大小,拉丁语系语言的字符,根本就不需要太宽的char,这样无区别的实现就造成了一定的浪费

在Java9中,我们引入了Compact Strings的设计,对字符串进行了大刀阔斧的改进,数据存储方式从char数组,改变为一个byte数组加上一个标识编码的所谓coder

5.23、如何理解clone对象 深拷贝和浅拷贝

返回一个object对象的复制。复制函数返回的是新的对象而不是一个引用。

有一个对象 A,在某一时刻 A 中已经包含了一些有效值,此时可能会需要一个和A完全相同新对象B,并且此后对B 任何改动都不会影响到A中的值。

A与B是两个独立的对象,但B的初始值是由A对象确定的。

new 一个对象的过程和 clone 一个对象的过程区别

new操作符的本意是分配内存。程序执行到new操作符时,首先去看new操作符后面的类型,因为知道了类型,才能知道要分配多大的内存空间

分配完内存之后,再调用构造函数,填充对象的各个域,这一步叫做对象的初始化,构造方法返回后,一个对象创建完毕,可以把他的引用

(地址)发布到外部,在外部就可以使用这个引用操纵这个对象。

clone在第一步是和new相似的,都是分配内存,调用clone方法时,分配的内存和原对象(即调用clone方法的对象)相同,

然后再使用原对象中对应的各个域,填充新对象的域,填充完成之后,clone方法返回,一个新的相同的对象被创建,同样可以把这个新对象的引用发布到外部

深拷贝和浅拷贝的原理如下图所示:

Person 中有两个成员变量,分别是name和 age, name 是String类型, age 是 int 类型

age是基本数据类型:直接将一个4字节的整数值拷贝过来就行

name是String类型的,它只是一个引用,指向一个真正的String对象

浅拷贝:stu = (Student)super.clone();

1、创建一个新对象,然后将当前对象的非静态字段(变量)复制该新对象

2、如果字段是值类型的,那么对该字段执行复制;

3、如果该字段是引用类型的话,则复制引用但不复制引用的对象。原始对象及其副本引用同一个对象;改变引用则一起改变

深拷贝:stu.addr = (Address)addr.clone();

1、copy对象所有的内部元素【对象、数组】,最后只剩下原始的类型(int)以及“不可变对象(String)

2、将对象序列化再读出来也是深拷贝

根据原Person对象中的name指向的字符串对象创建一个新的相同的字符串对象,将这个新字符串对象的引用赋给新拷贝的Person对象的name字段

如果想要深拷贝一个对象,这个对象必须要实现Cloneable接口,实现clone方法,并且在 clone 方法内部,把该对象引用的其他对象也要 clone 一份,

这就要求这个被引用的对象必须也要实现Cloneable接口并且实现clone方法。


5.24、==和equals的区别?

==比较的是两个基本变量的值是否相等,引用类型的地址

equals obj默认也是比较对象引用地址,重写后,比较对象内容

equals 方法必须满足自反性(x.equals(x)必须返回 true)、对称性(x.equals(y)返回 true 时,y.equals(x)
    也必须返回 true)、传递性(x.equals(y)和 y.equals(z)都返回 true 时,x.equals(z)也必须返回 true)和一致性(当
    x 和 y 引用的对象信息没有被修改时,多次调用 x.equals(y)应该得到同样的返回值),而且对于任何非 null值的引
    用 x,x.equals(null)必须返回false。
  • hashCode方法的作用?
    从object类中继承过来的,用于鉴定两个对象是否相等,object类的hashcode返回对象在内存中地址转换成的一个int值,(对象变整型)
    一般需要重写hashcode方法,在hsahmap中,可以用hashmap判断key是否重复。
    如果两个对象的equals返回true,那他们的hashCode必须相等,但是hashCode相等,不一定equals不一定相等
如果两个对象 x 和 y 满足 x.equals(y) == true,它们的哈希码(hashCode)应当相同。 
  (1)如果两个对象相同(equals 方法返回 true),那么它们的hashCode 值一定要相同;
  (2)如果两个对象的 hashCode 相同,它们并不一定相同。
  1. 使用==操作符检查"参数是否为这个对象的引用"
  2. 使用 instanceof 操作符检查"参数是否为正确的类型";
  3. 对于类中的关键属性,检查参数传入对象的属性是否与之相匹配;
  4. 编写完 equals 方法后,问自己它是否满足对称性、传递性、一致性;
  5. 重写 equals 时总是要重写 hashCode;
  6. 不要将 equals 方法参数中的 Object 对象替换为其他的类型,在重写时不要忘掉@Override 注解。
5.25、内部类和静态内部类的区别?

1、java中的内部类

  • 1、静态内部类:类的静态成员,存在于某个类的内部
  • 2、成员内部类:类的成员,存在于某个类的内部
  • 成员内部类可以调用外部类的所有成员,但只有在创建了外部类的对象后,才能调用外部的成员
  • 3、匿名内部类:存在于某个类的内部,是无类名的类
  • 4、局部内部类:存在于某个方法的内部,只能在方法内部中使用,一旦方法执行完毕,局部内部类就会从内存中删除
  • 必须注意:如果局部内部类中要使用他所在方法中的局部变量,那么就需要将这个局部变量定义为final的

2、各种内部类区别?

1、加载的顺序不同

  • 静态内部类比内部类先加载

2、静态内部类被static修饰,在类加载时JVM会把它放到方法区,被本类以及本类中所有实例所公用。

  • 定义在一个类内部的类叫内部类,内部类可以声明public、protected、private等访问限制,可以声明为abstract的供其他内部类或外部类继承与扩展,或者声明为static、final的,也可以实现特定的接口外部类按常规的类访问方式使用内部类

3、静态内部类只能够访问外部类的静态成员,而非静态内部类则可以访问外部类的所有成员(方法,属性)

  • 非静态内部类不能有静态成员(方法、属性)

4、静态内部类和非静态内部类在创建时有区别

//假设类A有静态内部类B和非静态内部类C,创建B和C的区别为:

A a=new A(); 
A.B b=new A.B(); 
A.C c=a.new C();

5.26、Java中一个字符占多少个字节,扩展再问int, long, double占多少字节
一个字符两个字节,              int 4 ,     long     double 8

5.27、final/finally/finalize的区别?

1、final:可修饰属性,成员方法和类,表示属性不可变《引用不可变,对象可变》,方法不可覆盖,类不可继承 一般基本类型string stringbuffer都是不能被继承的

final的使用场景?

  • 使用final修饰参数或者变量,也可以清楚地避免意外赋值导致的编程错误
  • final变量产生了某种程度的不可变(immutable)的效果,所以,可以用于保护只读数据,尤其是在并发编程中

5.28 final和Immutable区别?

final只能约束strList这个引用不可以被赋值,但是strList对象行为不被final影响,可以添加元素等操作

Immutable在很多场景是非常棒的选择,某种意义上说,Java语言目前并没有原生的不可变支持,如果要实现immutable的类,我们需要做到:

1、将class自身声明为final,这样别人就不能扩展来绕过限制了。

2、将所有成员变量定义为private和fnal,并且不要实现setter方法。

3、通常构造对象时,成员变量使用深度拷贝来初始化,而不是直接赋值,这是一种防御措施,因为你无法确定输入对象不被其他人修改。

4、如果确实需要实现getter方法,或者其他可能会返回内部状态的方法,使用copy-on-write原则,创建私有的copy

为什么匿名内部类访问局部变量时,局部变量为什么使用final修饰?

因为java inner class实际会copy一份,不是去直接使用局部变量,final防止出现数据一致性问题

  • 2、finally :异常处理的一部分,最终被执行(不要在finally代码块中处理返回值)(关闭jdbc连接,unlock操作)(政采云笔试做过这样的一道题)
    1.当try,catch,finally中都有return语句时,无论try中的语句有无异常,均返回finally中的return。
public static int getStr() {
       try {
           int str = 1/0;
           return str;
       } catch (Exception e) {
           return 2;
       } finally {
           return 3;
       }
}
public static void main(String[] args) {
    System.out.println(getStr());
}

执行结果:

java.lang.ArithmeticException: / by zero
  at com.test.frame.fighting.application.getStr(application.java:14)
  at com.test.frame.fighting.application.main(application.java:25)
3
Process finished with exit code 0

-----------------------------------------当改成try中无异常时---------------

public static int getStr() {
      try {
          return 1;
      } catch (Exception e) {
          return 2;
      } finally {
          return 3;
      }
}
public static void main(String[] args) {
    System.out.println(getStr());
}

运行结果:

3
Process finished with exit code 0

finally的一道题?(finally不会被执行的情况)(政采云)

try {
 // do something
     System.exit(1);//1、try-catch异常退出
 } fnally{
     System.out.println(“Print from fnally”);
 }//finally里面的代码可不会被执行的哦,这是一个特例
2、无限循环
try{
  while(ture){
    print(abc)
  }
}fnally{
  print(abc)
}
  • 3、线程被杀死 当执行 try, finally 的线程被杀死时。 fnally 也无法执行
    总结1:不要在finally中使用return语句。
    2:finally总是执行,除非程序或者线程被中断。
    finally的第二道题?
    政采云笔试做过这样的一道题,在try catch finally中都有return语句,最后会返回哪个代码块的return语句?
    当遇到return语句的时候,执行函数会立刻返回。但是,在Java语言中,如果存在finally就会有例外。除了return语句,try代码块中的break或continue语句
    也可能使控制权进入fnally代码块。最后返回的是finally中的代码块。
    注意点:如果在finally代码块中对函数返回的对象成员属性进行了修改,即使不在finally块中显式调用return语句,这个修改也会作用于返回值上
    3、finalize obj的方法,被垃圾回收时会调用回收对象的finalize方法 可以重写此方法来提供垃圾收集时的其他资源回收,关闭文件等(在jdk被标记为deprecated)
    缺点:1、无法保证finalize什么时候执行,执行的是否符合预期。使用不当会影响性能,导致程序死锁、挂起等。
    一旦实现了非空的fnalize方法,就会导致相应对象回收呈现数量级上的变慢,有人专门做过benchmark,大概是40~50倍的下降(因为JVM要对它进行额外处理)
    2、OOM原因之一:finalize拖慢垃圾收集,导致大量对象堆积。
    3、finalize还会掩盖资源回收时的出错信息(JDK的源代码,截取自java.lang.ref.Finalizer)
private void runFinalizer(JavaLangAccess jla) {
 // ... 省略部分代码
 try {
     Object fnalizee = this.get();
     if (fnalizee != null && !(fnalizee insanceof java.lang.Enum)) {
         jla.invokeFinalize(fnalizee);
         // Clear sack slot containing this variable, to decrease
         // the chances of false retention with a conservative GC
         fnalizee = null;
     }
 } catch (Throwable x) { }
     super.clear();
 }//这里的Throwable是被生吞了的!也就意味着一旦出现异常或者出错,你得不到任何有效信息
  • 有什么机制可以替换finalize?(虚引用)

Java平台目前在逐步使用java.lang.ref.Cleaner来替换掉原有的fnalize实现,利用虚引用和引用队列,我们可以保证对象被彻底销毁前做一些类似资源回收的工作,

比如关闭文件描述符(操作系统有限的资源),它比fnalize更加轻量、更加可靠,Cleaner适合作为一种最后的保证手段,而不是完全依赖Cleaner进行资源回收

第三方库自己直接利用虚引用定制资源收集(比如广泛使用的MySQL JDBC driver之一的mysql-connector-j,就利用了虚引用机制。)


5.30、Java源码
  • 1、从jdk的工具包开始,也就是《数据结构与算法》java版,如list接口和arraylist、linkedlist实现,hashmap和treemap等。
    这些数据结构也涉及到排序等算法。
  • 2、core包
    String、StringBuffer
    如果有一定的javaio基础,可以读fileReader等类,可以看《java in a Nutshell》,里面有整个javaIO的架构图
  • 3、javaIO包,是对继承和接口运用得最优雅的案例。如果将来做架构师,会经常与之打交道,如项目中部署和配置相关的核心类开发。读源码时,只需要读懂一些核心类即可,如和arraylist类似的二三十个类,对于每个类,也不一定要每个方法都读懂。像String有些方法已经到了虚拟机的层次(native方法),如hashcode方法。可以看看针对虚拟机的那套代码,如system classLoader的原理,他不在jdk包中,jdk是基于他的
  • 4、java web开发源码
    在阅读tomcat等源码之前,一定要有积累。
    1、写过一些servlet和jsp的代码
    2、看过《Servlet和JSP核心编程》
    3、看过sun公司的servlet规范
    4、看过http协议的rfc,debug过http的数据包
    然后可以读struts2的源码,然后可以读tomcat的源码《how tomcat works》
    他会告诉你httpServletRequest如何在容器内部实现的,tomcat如何通过socket来接受外面的请求,你的servlet代码如何被tomcat容器调用的(回调)
  • 5、java数据库源码阅读
    先度sun公司JDBC的规范
    mysql的jdbc驱动,因为他开源、设计优雅,如果你了解这些内幕,那么在学习hibernate和mybatis等持久化框架时,就会得心应手很多。
    读完了jdbc驱动,可以读读数据库了,用java语言开发的数据库Hsqldb
  • 6、java通讯以及客户端软件
    推荐即时通讯软件wildfire和spark。可以把wildfire理解成MSN服务器,Spark理解成MSN客户端。他们是通过XMPP协议通讯的。
    原因:
    1、XMPP轻量级,好理解
    2、学习socket通讯实现,特别是C/S架构设计
    3、模式化设计。他们都是基于module的,既可以了解模块化架构,还可以了解模块化的技术支撑:java虚拟机的classLoader的引用场景
    4、Event Driven架构
  • 7、java企业级应用
    在读Spring源码前,一定要看rod johnson写的《j2ee design and development》,他是Spring的设计思路。
    在读源码前,你会发现他们用到很多第三方jar包,最好把哪些jar包先一个个搞明白。
    工作流:jbpm的源码,网上有介绍jbpm内核的文章,在读工作流源码前,一定要对其理论模型有深入的了解,以及写一些demo、或做过一些项目。

5.31 一个接口有多个实现类,当调用接口时,如何判断用的哪个实现类?202103补
  • 1、直接new一个实例,这样肯定知道用的是哪个实例
  • 2、定义接口类型的变量,用某个实例去初始化(常用)
    举例子
    A接口有个eat方法,A1、A2、A3分别实现A接口,A1吃饭 A2吃鱼 A3吃肉
    需要得到“吃鱼”,A a = new A2();
    需要得到“吃肉”,A a = new A3();
//接口:
public interface CsBaseService {
//获得总记录条数
  public int getTotalCount(JDBCBean jdbcBean);
  }
}
//实现类1:
@Service
public class CsLastUpdateService implements CsBaseService {
    @Override
    public int getTotalCount(JDBCBean jdbcBean) {
        return 0;
    }  
}
//实现类2:
public class CsRelateModelService implements CsBaseService {
    @Override
    public int getTotalCount(JDBCBean jdbcBean) {
        return 2;
   }
}
//调用的时候:
public class RelateModelController  extends BaseController{
      @Autowired
      private CsRelateModelService relateModelService;//自动装配实现类2
      initParamProcess(relateModelService,new RelateModel(),new Page());//初始化实现类2,关键在这步,指定relateModelService为beaseService,具体见BaseController类
      int totalCount = beaseService.getTotalCount(jdbcBean);//然后直接调用实现类2的方法,输出为2
}
//抽象类   RelateModelController 的父类BaseController
public abstract class BaseController {
       void initParamProcess(CsBaseService beaseService, JDBCBean jdbcBean,Page page) {
      this.beaseService = beaseService;  //指定哪个实现类为beaseService
      this.jdbcBean = jdbcBean;
      this.page = page;
     }
}
5.32、Java中的委派模式(Delegate)

委派模式(Delegate)是面向对象设计模式中常用的一种模式。

  • 这种模式的原理为类B和类A是两个互相没有任何关系的类,B具有和A一模一样的方法和属性;并且调用B中的方法,属性就是调用A中同名的方法和属性。B好像就是一个受A授权委托的中介。第三方的代码不需要知道A的存在,也不需要和A发生直接的联系,通过B就可以直接使用A的功能,这样既能够使用到A的各种公能,又能够很好的将A保护起来了。一举两得,岂不很好!
  • 下面用一个很简单的例子来解释下
class A{
    void method1(){...}
    void method2(){...}
}
class B{
    //delegation
    A a = new A();
   //method with the same name in A
    void method1(){ a.method1();}
    void method2(){ a.method2();}
    //other methods and attributes
    ...
}
public class Test{
     public static void main(String args[]){
    B b = new B();
    b.method1();//invoke method2 of class A in fact
    b.method2();//invoke method1 of class A in fact
    }
}
5.33、采用单例模式还是静态方法?20210707补

观点一:(单例)

单例模式比静态方法有很多优势:

  • 首先,单例可以继承类,实现接口,而静态类不能(可以集成类,但不能集成实例成员);
  • 其次,单例可以被延迟初始化,静态类一般在第一次加载是初始化;
  • 再次,单例类可以被集成,他的方法可以被覆写;
  • 最后,或许最重要的是,单例类可以被用于多态而无需强迫用户只假定唯一的实例。举个例子,你可能在开始时只写一个配置,但是以后你可能需要支持超过一个配置集,或者可能需要允许用户从外部文件中加载一个配置对象,或者编写自己的。你的代码不需要关注全局的状态,因此你的代码会更加灵活。

观点二:(静态方法)

  • 静态方法中产生的对象,会随着静态方法执行完毕而释放掉,而且执行类中的静态方法时,不会实例化静态方法所在的类。如果是用singleton, 产生的那一个唯一的实例,会一直在内存中,不会被GC清除的(原因是静态的属性变量不会被GC清除),除非整个JVM退出了。

观点三:(Good!)

  • 由于DAO的初始化,会比较占系统资源的,如果用静态方法来取,会不断地初始化和释放,所以我个人认为如果不存在比较复杂的事务管理,用singleton会比较好。
    总结:大家对这个问题都有一个共识:那就是实例化方法更多被使用和稳妥,静态方法少使用。

有时候我们对静态方法和实例化方法会有一些误解。

1、大家都以为“ 静态方法常驻内存,实例方法不是,所以静态方法效率高但占内存。”

  • 事实上,他们都是一样的,在加载时机和占用内存上,静态方法和实例方法是一样的,在类型第一次被使用时加载。调用的速度基本上没有差别。

2、大家都以为“ 静态方法在堆上分配内存,实例方法在堆栈上”

  • 事实上所有的方法都不可能在堆或者堆栈上分配内存,方法作为代码是被加载到特殊的代码内存区域,这个内存区域是不可写的。

方法占不占用更多内存,和它是不是static没什么关系。

因为字段是用来存储每个实例对象的信息的,所以字段会占有内存,并且因为每个实例对象的状态都不一致(至少不能认为它们是一致的),所以每个实例对象的所以字段都会在内存中有一分拷贝,也因为这样你才能用它们来区分你现在操作的是哪个对象。

但方法不一样,不论有多少个实例对象,它的方法的代码都是一样的,所以只要有一份代码就够了。因此无论是static还是non-static的方法,都只存在一份代码,也就是只占用一份内存空间。

同样的代码,为什么运行起来表现却不一样?这就依赖于方法所用的数据了。主要有两种数据来源,一种就是通过方法的参数传进来,另一种就是使用class的成员变量的值。

3、大家都以为“实例方法需要先创建实例才可以调用,比较麻烦,静态方法不用,比较简单”

  • 事实上如果一个方法与他所在类的实例对象无关,那么它就应该是静态的,而不应该把它写成实例方法。所以所有的实例方法都与实例有关,既然与实例有关,那么创建实例就是必然的步骤,没有麻烦简单一说。
    当然你完全可以把所有的实例方法都写成静态的,将实例作为参数传入即可,一般情况下可能不会出什么问题。

从面向对象的角度上来说,在抉择使用实例化方法或静态方法时,应该根据是否该方法和实例化对象具有逻辑上的相关性,如果是就应该使用实例化对象 反之使用静态方法。这只是从面向对象角度上来说的。

如果从线程安全、性能、兼容性上来看 也是选用实例化方法为宜。

我们为什么要把方法区分为:静态方法和实例化方法 ?

  • 如果我们继续深入研 究的话,就要脱离技术谈理论了。早期的结构化编程,几乎所有的方法都是“静态方法”,引入实例化方法概念是面向对象概念出现以后的事情了,区分静态方法和 实例化方法不能单单从性能上去理解,创建c++、java、c# 这样面向对象语言的大师引入实例化方法一定不是要解决什么性能、内存的问题,而是为了让开发更加模式化、面向对象化。这样说的话,静态方法和实例化方式的区分是为了解决模式的问题。

5.34 贫血模型与充血模型 20210706补

贫血模型与充血模型

见设计模式


6、序列化的原理,序列化是怎么实现的?(20181113)

  • 序列化的背景:
  • Java对象在JVM运行时被创建、更新和销毁,当JVM退出时,对象也会随之销毁,即这些对象的生命周期不会比JVM的生命周期更长。但在现实应用中,我们常常需要将对象及其状态在多个应用之间传递、共享,或者将对象及其状态持久化,在其他地方重新读取被保存的对象及其状态继续进行处理。这就需要通过将Java对象序列化来实现。
  • 在使用Java序列化技术保存对象及其状态信息时,对象及其状态信息会被保存在一组字节数组中,在需要时再将这些字节数组反序列化为对象。注意,对象序列化保存的是对象的状态,即它的成员变量,因此类中的静态变量不会被序列化。
  • 对象序列化除了用于持久化对象,在RPC(远程过程调用)或者网络传输中也经常被使用
  • 什么是序列化
  • 序列化:指的是将java对象转换为二进制流的过程
  • 反序列化:将二进制流恢复成对象的过程
  • 序列化的解决方案:
  • java内置的序列化方式:效率较低
  • hessian:效率比protocal buffers稍低
  • json和xml:应用广泛
  • 序列化的作用:
  • 将对象通过网络传输到远端
6.1、Java序列化API的使用

Java序列化API为处理对象序列化提供了一个标准机制,具体的Java系列化需要注意以下事项。

◎ 类要实现序列化功能,只需实现java.io.Serializable接口即可。

◎ 序列化和反序列化必须保持序列化的ID一致,一般使用private static final long serialVersionUID定义序列化ID。

◎ 序列化并不保存静态变量。

◎ 在需要序列化父类变量时,父类也需要实现Serializable接口。

◎ 使用Transient关键字可以阻止该变量被序列化,在被反序列化后,transient变量的值被设为对应类型的初始值,例如,int类型变量的值是0,对象类型变量的值是null。

◎ 当一个父类实现序列化、子类自动实现序列化、不需要显示实现serializable接口

◎ 若该对象的实例变量引用其他对象,序列化该对象也把引用对象进行序列化

具体的序列化实现代码如下,以下代码通过implements Serializable实现了一个序列化的类。注意,transient 修饰的属性和 static 修饰的静态属性不会被序列化

对象通过序列化后在网络上传输时,基于网络安全,我们可以在序列化前将一些敏感字段(用户名、密码、身份证号码)使用秘钥进行加密,在反序列化后再基于秘钥对数据进行解密。这样即使数据在网络中被劫持,由于缺少秘钥也无法对数据进行解析,这样可以在一定程度上保证序列化对象的数据安全。

6.2、Java序列化API的使用

在Java生态中有很多优秀的序列化框架,比如arvo、protobuf、thrift、fastjson。我们也可以基于JDK原生的ObjectOutputStream和ObjectInputStream类实现对象进行序列化及反序列化,并调用其writeObject和readObject方法实现自定义序列化策略。具体的实现代码如下

以下代码通过文件流的方式将 zhangsan对象的状态写入磁盘中,在需要使用的时候再以文件流的方式将其读取并反序列化成我们需要的对象及其状态数据

  • java内置序列化方式:(实现了serializable接口)
//关键代码
//定义一个字节数组输出流
ByteArrayOutputStream os = new ByteArrayOutputStream();
//对象输出流
ObjectOutputStream out = new ObjectOutputStream(os);
//将对象写入到直接数组输出,进行序列化
out.writeObject(zhangsan);//将person类实例zhangsan序列化为字节数组
byte[] zhangsanByte = os.toByteArray();
//字节数组输入流
ByteArrayInputStream is = new ByteArrayInputStream(zhangsanByte);
//执行反序列化,从流中读取对象
ObjectInputStream in = new ObjectInput(is);
Persoon person = (Person)in.readObject();//反序列化
//Hessian序列化方案
需要引入提供的包hessian-4.0.7.jar,关键代码如下
ByteArrayOutputStream os = new ByteArrayOutputStream();
//hessian的序列化输出
HessianOutput ho = new HessianOutput(os);
ho.writeObject(zhangsan);//将person类实例zhangsan序列化为字节数组
byte[] zhangsanByte = os.toByteArray();
//字节数组输入流
ByteArrayInputStream is = new ByteArrayInputStream(zhangsanByte);
//执行反序列化,从流中读取对象
HessianInput in = new HessianInput(is);
Persoon person = (Person)in.readObject();//反序列化

7、内部类

定义在类内部的类被称为内部类。内部类根据不同的定义方式,可分为静态内部类、成员内部类、局部内部类和匿名内部类这4种。

7.1、静态内部类

定义在类内部的静态类被称为静态内部类。静态内部类可以访问外部类的静态变量和方法;在静态内部类中可以定义静态变量、方法、构造函数等;静态内部类通过“外部类.静态内部类”的方式来调用,具体的实现代码如下

上面的代码通过public static class StaticInnerClass{}代码块定义了一个静态内部类StaticInnerClass,然后定义了静态内部类的getClassName方法,在使用的过程中通过“外部类.静态内部类”的方式进行调用。

这样就定义一个静态内部类并可以像普通类那样调用静态内部类的方法。Java集合类HashMap在内部维护了一个静态内部类Node数组用于存放元素,但Node数组对使用者是透明的。像这种和外部类关系密切且不依赖外部类实例的类,可以使用静态内部类实现。

7.2、成员内部类

定义在类内部的非静态类叫作成员内部类,成员内部类不能定义静态方法和变量(final修饰的除外),因为成员内部类是非静态的,而在Java的非静态代码块中不能定义静态方法和变量。成员内部类具体的实现代码如下:

从上述代码可以看到,在OutClass中通过public class MemberInnerClass定义了一个成员内部类,其使用方式和静态内部类相同。

7.3、局部内部类

定义在方法中的类叫作局部内部类。当一个类只需要在某个方法中使用某个特定的类时,可以通过局部类来优雅地实现,具体的实现代码如下:

以上代码在partClassTest方法中通过class PastClass{}语句块定义了一个局部内部类。

7.4、匿名内部类

匿名内部类指通过继承一个父类或者实现一个接口的方式直接定义并使用的类。匿名内部类没有class关键字,这是因为匿名内部类直接使用new生成一个对象的引用。具体的实现代码如下:

在以上代码中首先定义了一个抽象类Worker和一个抽象方法workTime,然后定义了一个Test类,在Test类中定义了一个方法,该方法接收一个Worker参数,这时匿名类需要的准备工作都已做好。在需要一个根据不同场景有不同实现的匿名内部类时,直接在test方法中新建匿名内部类并重写相关方法即可。

Action1、避免通过一个类的对象引用访问此类的静态变量或静态方法,无谓增加编译器解析成本,直接用类名来访问即可。

Action2、所有的覆写方法,必须加@Override注解。

  • getObject() 与get0bject()的问题。一个是字母的O,一个是数字的0,加@Override 可以准确判断是否覆盖成功。另外,如果在抽象类中对方法签名进行修改,其实现类会马上编译报错。

Action3、相同参数类型,相同业务含义,才可以使用Java的可变参数,避免使用Object。

  • 说明:
  • 可变参数必须放置在参数列表的最后。(尽量不用可变参数编程)
  • 正例:
public User getUsers(String type, Integer...ids) {
  ...
}

Action4、外部正在调用或者二方库依赖的接口,不允许修改方法签名,避免对接口调用方产生影响。接口过时必须加@Deprecated注解,并清晰地说明采用的新接口或者新服务是什么?

  • 吃过这个亏,以后不会踩这个坑了~

Action5、不能使用过时的类或方法

  • 这是是强制执行的,切记
  • 说明:java.net.URLDecoder 中的方法 decode(String encodeStr) 这个方法已经过时,应该使用双参数 decode(String source, String encode)。 接口提供方既然明确是过时接口,那么有义务同时提供新的接口;作为调用方来说,有义务去考证过时方法的新实现是什么。

Action6、Object的equals方法容易抛空指针异常,应使用常量或确定有值的对象来调用equals。

  • 正例:"test".equals(object);
  • 反例:object.equals("test");
  • 说明:推荐使用 java.util.Objects#equals(JDK7引入的工具类)

Action7、所有的相同类型的包装类对象之间值的比较,全部使用equals 方法比较。

  • 说明:对于 Integer var = ? 在-128至127范围内的赋值,Integer对象是在IntegerCache.cache
    产生,会复用已有对象,这个区间内的 Integer值可以直接使用 ==进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,这是一个大坑,推荐使用 equals方法进行判断。

Action8、关于基本数据类型与包装数据类型的使用标准如下:

1)所有的 POJO 类属性必须使用包装数据类型。

2)RPC 方法的返回值和参数必须使用包装数据类型。

3)所有的局部变量使用基本数据类型。

说明:POJO类属性没有初值是提醒使用者在需要使用时,必须自己显式地进行赋值,任何

NPE问题,或者入库检查,都由使用者来保证。

  • 正例:数据库的查询结果可能是null,因为自动拆箱,用基本数据类型接收有NPE风险。
  • 反例:比如显示成交总额涨跌情况,即正负x%,x为基本数据类型,调用的RPC服务,调用不成功时,返回的是默认值,页面显示为0%,这是不合理的,应该显示成中划线。所以包装数据类型的null值,能够表示额外的信息,如:远程调用失败,异常退出。

Action9、定义 DO/DTO/VO 等 POJO 类时,不要设定任何属性默认值。

  • 反例: POJO 类的 gmtCreate 默认值为 new Date();但是这个属性在数据提取时并没有置入具体值,在更新其它字段时又附带更新了此字段,导致创建时间被修改成当前时间。

Action10、序列化类新增属性时,请不要修改 serialVersionUID 字段,避免反序列失败; 如果完全不兼容升级,避免反序列化混乱,那么请修改 serialVersionUID 值。

  • 说明: 注意 serialVersionUID 不一致会抛出序列化运行时异常。

Action11、构造方法里面禁止加入任何业务逻辑,如果有初始化逻辑,请放在 init 方法中。

Action12、POJO 类必须写 toString 方法。使用 IDE 的中工具: source> generate toString 时,如果继承了另一个 POJO 类,注意在前面加一下 super.toString。

  • 说明: 在方法执行抛出异常时,可以直接调用 POJO 的 toString()方法打印其属性值,便于排查问题

Action13、使用索引访问用 String 的 split 方法得到的数组时,需做最后一个分隔符后有无内容的检查,否则会有抛IndexOutOfBoundsException 的风险。

说明:

String str = "a,b,c,,";
String[] ary = str.split(",");
// 预期大于 3,结果是 3
System.out.println(ary.length);

Action14、当一个类有多个构造方法,或者多个同名方法,这些方法应该按顺序放置在一起,便于阅读,此条规则优先于第 15 条规则

正例:

public int method(int param);
protected double method(int param1, int param2);
private void method();

Action15、类内方法定义顺序依次是:公有方法或保护方法 > 私有方法 > getter/setter方法

  • 说明:
  • 公有方法是类的调用者和维护者最关心的方法,首屏展示最好;
  • 保护方法虽然只是子类关心,也可能是“模板设计模式”下的核心方法;
  • 而私有方法外部一般不需要特别关心,是一个黑盒实现;
  • 因为承载的信息价值较低,所有 Service 和 DAO 的 getter/setter 方法放在类体最后。

Action16、setter 方法中,参数名称与类成员变量名称一致, this.成员名 = 参数名。在getter/setter 方法中, 不要增加业务逻辑,增加排查问题的难度

反例:

public Integer getData() {
  if (true) {
    return this.data + 100;
  } else {
    return this.data - 100;
  }
}

Action17、循环体内,字符串的连接方式,使用 StringBuilder 的 append 方法进行扩展。

反例:

String str = "start";
for (int i = 0; i < 100; i++) {
  str = str + "hello";
}

说明: 反编译出的字节码文件显示每次循环都会 new 出一个 StringBuilder 对象,然后进行append 操作,最后通过 toString 方法返回 String 对象,造成内存资源浪费。

Action18、final 可以声明类、成员变量、方法、以及本地变量,下列情况使用 final 关键字:

1) 不允许被继承的类,如: String 类。

2) 不允许修改引用的域对象,如: POJO 类的域变量。

3) 不允许被重写的方法,如: POJO 类的 setter 方法。

4) 不允许运行过程中重新赋值的局部变量。

5) 避免上下文重复使用一个变量,使用 final 描述可以强制重新定义一个变量,方便更好地进行重构

Action19、慎用 Object 的 clone 方法来拷贝对象。

  • 说明: 对象的 clone 方法默认是浅拷贝,若想实现深拷贝需要重写 clone 方法实现属性对象的拷贝。

Action20、类成员与方法访问控制从严:

1) 如果不允许外部直接通过 new 来创建对象,那么构造方法必须是 private。

2) 工具类不允许有 public 或 default 构造方法。

3) 类非 static 成员变量并且与子类共享,必须是 protected。

4) 类非 static 成员变量并且仅在本类使用,必须是 private。

5) 类 static 成员变量如果仅在本类使用,必须是 private

6) 若是 static 成员变量,必须考虑是否为 final。

7) 类成员方法只供类内部调用,必须是 private。

8) 类成员方法只对继承类公开,那么限制为 protected。

  • 说明: 任何类、方法、参数、变量,严控访问范围。过于宽泛的访问范围,不利于模块解耦。
  • 思考:如果是一个 private 的方法,想删除就删除,可是一个 public 的 service 方法,或者
    一个 public 的成员变量,删除一下,不得手心冒点汗吗?变量像自己的小孩,尽量在自己的视线内,变量作用域太大, 无限制的到处跑,那么你会担心的。

Action21、禁止在POJO类中,同时存在对应属性xxx 的isXxx()和getXxx()方法

  • 说明:框架在调用属性Xxx的提取方法时,并不能确定哪个方法一定是被优先调用到的。

Action22、在使用正则表达式时, 利用好其预编译功能, 可以有效加快正则匹配速度。

说明: 不要在方法体内定义: Pattern pattern = Pattern.compile("规则");

Action23、避免用 ApacheBeanutils 进行属性的 copy。

说明: ApacheBeanUtils 性能较差, 可以使用其他方案比如 SpringBeanUtils, CglibBeanCopier, 注意均是浅拷贝。

Action24、注意 Math.random() 这个方法返回是 double 类型,注意取值的范围 0 ≤ x < 1(能够取到零值,注意除零异常),如果想获取整数类型的随机数,不要将 x 放大 10 的若干倍然后取整, 直接使用 Random 对象的 nextInt 或者 nextLong 方法。

Action25、枚举 enum(括号内)的属性字段必须是私有且不可变

相关文章
|
5月前
|
存储 Java 容器
Java基本语法详解
本文深入讲解了Java编程的基础语法,涵盖数据类型、运算符、控制结构及数组等核心内容,帮助初学者构建坚实的编程基础。
|
4月前
|
存储 SQL NoSQL
Redis-常用语法以及java互联实践案例
本文详细介绍了Redis的数据结构、常用命令及其Java客户端的使用,涵盖String、Hash、List、Set、SortedSet等数据类型及操作,同时提供了Jedis和Spring Boot Data Redis的实战示例,帮助开发者快速掌握Redis在实际项目中的应用。
333 1
Redis-常用语法以及java互联实践案例
|
4月前
|
Java
Java基础语法与面向对象
重载(Overload)指同一类中方法名相同、参数列表不同,与返回值无关;重写(Override)指子类重新实现父类方法,方法名和参数列表必须相同,返回类型兼容。重载发生在同类,重写发生在继承关系中。
179 1
|
6月前
|
Java 数据库连接 数据库
Java 相关知识点总结含基础语法进阶技巧及面试重点知识
本文全面总结了Java核心知识点,涵盖基础语法、面向对象、集合框架、并发编程、网络编程及主流框架如Spring生态、MyBatis等,结合JVM原理与性能优化技巧,并通过一个学生信息管理系统的实战案例,帮助你快速掌握Java开发技能,适合Java学习与面试准备。
290 2
Java 相关知识点总结含基础语法进阶技巧及面试重点知识
|
5月前
|
算法 Java 测试技术
零基础学 Java: 从语法入门到企业级项目实战的详细学习路线解析
本文为零基础学习者提供完整的Java学习路线,涵盖语法基础、面向对象编程、数据结构与算法、多线程、JVM原理、Spring框架、Spring Boot及项目实战,助你从入门到进阶,系统掌握Java编程技能,提升实战开发能力。
303 0
|
Java Apache Maven
Java百项管理之新闻管理系统 熟悉java语法——大学生作业 有源码!!!可运行!!!
文章提供了使用Apache POI库在Java中创建和读取Excel文件的详细代码示例,包括写入数据到Excel和从Excel读取数据的方法。
176 6
Java百项管理之新闻管理系统 熟悉java语法——大学生作业 有源码!!!可运行!!!
|
6月前
|
存储 安全 Java
从基础语法到实战应用的 Java 入门必备知识全解析
本文介绍了Java入门必备知识,涵盖开发环境搭建、基础语法、面向对象编程、集合框架、异常处理、多线程和IO流等内容,结合实例帮助新手快速掌握Java核心概念与应用技巧。
147 0
|
Java 开发工具 Android开发
Kotlin语法笔记(26) -Kotlin 与 Java 共存(1)
Kotlin语法笔记(26) -Kotlin 与 Java 共存(1)
151 2
|
Java 开发工具 Android开发
Kotlin语法笔记(26) -Kotlin 与 Java 共存(1)
本系列教程笔记详细讲解了Kotlin语法,适合需要深入了解Kotlin的开发者。若需快速学习Kotlin,建议查看“简洁”系列教程。本期重点介绍了Kotlin与Java的共存方式,包括属性、单例对象、默认参数方法、包方法、扩展方法以及内部类和成员的互操作性。通过这些内容,帮助你在项目中更好地结合使用这两种语言。
175 1
|
10月前
|
缓存 安全 Java
java面试-基础语法与面向对象
本文介绍了 Java 编程中的几个核心概念。首先,详细区分了方法重载与重写的定义、发生阶段及规则;其次,分析了 `==` 与 `equals` 的区别,强调了基本类型和引用类型的比较方式;接着,对比了 `String`、`StringBuilder` 和 `StringBuffer` 的特性,包括线程安全性和性能差异;最后,讲解了 Java 异常机制,包括自定义异常的实现以及常见非检查异常的类型。这些内容对理解 Java 面向对象编程和实际开发问题解决具有重要意义。