0
点赞
收藏
分享

微信扫一扫

Java基础知识第一讲:基础语法

Java基础知识第一讲:基础语法

文章目录

推荐书籍

为何学习 Java 基础

  • Java 基础是学习Java EE、大数据、Android开发的基石
  • Java EE
    • 在这里插入图片描述
  • 大数据
    • 在这里插入图片描述
  • Android 开发
    • 在这里插入图片描述

1、Java 编程语言核心结构

Java基础知识图解
在这里插入图片描述
计算机语言:人与计算机交流的方式
种类:
第一类语言:机器语言。指令以二级制代码形式存在。
第二类语言:汇编语言。使用助记符表示一条机器指令。

  • 在这里插入图片描述

第三代语言:高级语言

  • C、Pascal、Fortran 面向过程的语言
  • C++ 面向过程/面向对象
  • Java 跨平台的纯面向对象语言(现阶段市场需求最多
  • .Net 跨语言的平台
  • Python、Scala等等

1.1、Java 语言概述

概述

  • 由Sun 公司1995年推出的一门高级编程语言
  • 是一种面向Internet 的编程语言
  • 随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言
    • 后台开发使用的语言:Java、PHP、Python、Go、Node.js

Java 在各领域的应用

  • 企业级应用:主要指复杂的大企业软件系统、各种类型的网站。优势:安全机制及跨平台,应用领域包括:金融、电信、交通、电子商务等;
  • Android 平台应用:Android应用程序使用Java语言编写;
  • 大数据平台开发:各类框架有Hadoop, spark, storm, flink等,就这类技术生态圈来讲,还有各种中间件如flume, kafka, sqoop等等 ,这些框架以及工具大多数是用Java 编写而成
  • 移动领域应用:主要表现在消费和嵌入式领域,例如:手机、 PDA、机顶盒、汽车通信设备等

Java语言的诞生

  • James Gosling 团队开发“Green” 项目是,发现 C 缺少垃圾回收系统,可移植的安全性、分布式程序设计和多线程功能
  • Java 语言的变量声明,操作符形式,参数传递,流程控制等方面和 C、C++ 语言完全相同,继承了C++ 语言面向对象技术的核心,舍弃了C语言中容易引起错误的指针(使用引用来取代)、运算符重载、多重继承(使用接口取代),增加了垃圾回收器功能。
  • JDK 1.5引入泛型编程、类型安全的枚举、不定长参数和自动装箱/拆箱

主要特性

  • Java语言是强制面向对象的,支持类之间的单继承,支持接口之间的多继承
  • Java语言是分布式的。Java语言支持Internet应用的开发,在基本的Java应用编程接口中有一个网络应用编程接口;
  • Java语言是健壮的。 Java的强类型机制、异常处理、垃圾的自动收集等是Java程序健壮性的重要保证;
  • Java语言是安全的。 Java通常被用在网络环境中,为此, Java提供了一个安全机制以防恶意代码的攻击。如:安全防范机制(类ClassLoader),如分配不同的名字空间以防替代本地的同名类、字节代码检查;
  • Java语言是体系结构中立的。 Java程序(后缀为java的文件)在Java平台上被编译为体系结构中立的字节码格式(后缀为class的文件),然后可以在实现这个Java平台的任何系统中运行;

1.2、Java语言运行机制及运行过程

Java 语言特点
特点一: 面向对象

  • 两个基本概念:类、对象
  • 三大特性:封装、继承、多态

特点二: 健壮性

  • 吸收了C/C++语言的优点,但去掉了其影响程序健壮性的部分(如指针、内存的申请与释放等),提供了一个相对安全的内存管理和访问机制

特点三: 跨平台性

  • 跨平台性:通过Java语言编写的应用程序在不同的系统平台上都可以运行。 “Write once , Run Anywhere”
  • 原理:只要在需要运行 Java 应用程序的操作系统上,先安装一个Java虚拟机 (JVM Java
    Virtual Machine) 即可。由 JVM 来负责 Java 程序在该系统中的运行。
  • 在这里插入图片描述

两种核心机制:

  • Java 虚拟机

    • JVM是一个虚拟的计算机,具有指令集并使用不同的存储区域。负责执行指令,管理数据、内存、寄存器。
    • 对于不同的平台,有不同的虚拟机
    • 只有某平台提供了对应的java虚拟机, java程序才可在此平台运行
    • Java 虚拟机机制屏蔽了底层运行平台的差别,实现了“一次编译,到处运行”
  • 在这里插入图片描述

  • Java 虚拟机所处的位置
    在这里插入图片描述

  • 垃圾回收机制

    • 对不再使用的内存空间进行回收—— 垃圾回收,在C/C++ 等语言中,由程序员负责回收无用内存,java 语言消除了程序员回收无用内存空间的责任:它提供一种系统级线程 跟踪存储空间的分配方案,并在JVM空闲时,检查并释放那些可被释放的存储空间。
    • 垃圾回收在 Java 程序运行过程中自动进行,程序员无法精确控制和干预

    Action:Java程序还会出现内存泄漏和内存溢出问题吗? 是的

1.3、环境搭建

1、什么是JDK, JRE

  • JDK(Java Development Kit Java开发工具包)JDK 是提供给 Java 开发人员使用的,其中包含了java的开发工具,也包括了 JRE。其中的开发工具:编译工具(javac.exe) 打包工具(jar.exe)等
  • JRE(Java Runtime Environment Java运行环境) 包括Java虚拟机和 Java 程序所需的核心类库等,如果想要运行一个开发好的Java程序,计算机中只需要安装JRE即可。
  • 总结:使用JDK的开发工具完成的java程序,然后交给JRE去运行
  • JDK = JRE + 开发工具集(例如Javac编译工具等)
  • JRE = JVM + Java SE标准类库在这里插入图片描述
    2、配置环境变量 path
  • 可以参考网上资料

3、开发HelloWord程序

public class Test{
	// 程序执行的入口
	public static void main(String[] args) {
		System.out.println(“Hello World!);
	}
}

在这里插入图片描述

1.4、基本语法

1、文档注释(Java 特有)

/**
* @author 指定java程序的作者
* @version 指定源文件的版本
*/

注释内容可以被JDK提供的工具 javadoc 所解析,生成一套以网页文件形式体现该程序的说明文档
操作方式

javadoc -d mydoc -author -version HelloWorld.java

2、基本语法

  • Java 源文件以“java”为扩展名。源文件的基本组成部分是类(class)

  • Java 应用程序的执行入口是main()方法。它有固定的书写格式:

    • public static void main(String[] args) {…}
  • Java 语言严格区分大小写

  • Java 方法由一条条语句构成,每个语句以“;”结束。

  • 大括号都是成对出现的, 缺一不可。

  • 一个源文件中最多只能有一个 public 类。其它类的个数不限,如果源文件包含一个public类,则文件名必须按该类名命名

3、良好的编程风格
正确的注释和注释风格

  • 使用文档注释来注释整个类或整个方法。
  • 如果注释方法中的某一个步骤,使用单行或多行注释。

正确的缩进和空白

  • 使用一次tab操作,实现缩进
  • 运算符两边习惯性各加一个空格。比如: 2 + 4 * 5。

块的风格

  • Java API 源代码选择了行尾风格
public class Test {
	public static void main(String[] args){
		System.out.println("Block Style!");
	}
}

4、常用的Java开发工具
文本编辑工具

  • 记事本
  • UltraEdit
  • EditPlus
  • TextPad
  • NotePad
  • Sublime

Java 集成开发环境(IDE)

  • JBuilder
  • NetBeans
  • Eclipse
  • MyEclipse
  • IntelliJ IDEA(目前Java开发效率最快的IDE工具)

2、变量与运算符

2.1、关键字与保留字

关键字(keyword)的定义和特点:
定义: 被Java语言赋予了特殊含义,用做专门用途的字符串(单词)
特点: 关键字中所有字母都为小写

  • 用于定义数据类型的关键字

  • 用于定义流程控制的关键字

  • 用于定义访问权限修饰符的关键字

  • 用于定义类,函数,变量修饰符的关键字

  • 用于定义类与类之间关系的关键字

  • 用于定义建立实例及引用实例,判断实例的关键字

  • 用于异常处理的关键字

  • 用于包的关键字

  • 其他修饰符关键字

  • 用于定义数据类型值的字面值

保留字:现有Java版本尚未使用, 但以后版本可能会作为关键字使用。自己命名标识符时要避免使用这些保留字。

2.2、标识符(Identifier)

标识符定义:Java 对各种变量、 方法和类等要素命名时使用的字符序列称为标识符

  • 凡是自己可以起名字的地方都叫标识符
  • 由26个英文字母大小写,0~9,_或$组成,数字不可以开头,严格区分大小写,不能包含空格

名称命名规范

  • 包名:多单词组成时,所有字母都小写:xxxyyyzzz
  • 类名、接口名:多单词组成时,所有单词的首字母大写:XxxYyyZxx
  • 变量名、方法名:多单词时,第一个单词首字母小写,第二个单词开始每个单词首字母大写:xxxYyyZzz
  • 常量名:所有字母都大写。多单词时每个单词用下划线连接:XXX_YYY_ZZZ
  • 注意:标识符可以使用汉字声明,但是不建议使用

2.3、变量

变量的概念:

  • 内存中的一个存储区域
  • 该区域的数据可以在同一类型范围内不断变化
  • 变量是程序中最基本的存储单元。包含变量类型、变量名和存储的值

变量的作用:用于在内存中保存数据
使用变量注意事项:

  • Java中每个变量必须先声明,后使用
  • 使用变量名来访问这块区域的数据
  • 变量的作用域:其定义所在的一对{ }内
  • 变量只有在其作用域内才有效
  • 同一个作用域内,不能定义重名的变量

声明和赋值变量

  • 语法: <数据类型> <变量名> = <初始化值>
  • 例如: int var = 10;

变量分类-按数据类型

  • 基本数据类型
    • 数值型 整数(byte,short,int,long) 浮点类型(float,double)
      • Java各整数类型有固定的表数范围和字段长度(bit 计算机中的最小存储单位 byte计算机中基本存储单元)
        • byte 1字节(8bit) -128~127
        • short 2字节 -2^15 ~ 2^15 -1
        • int 4字节 -2^31 ~2^31 -1
        • long 8字节 -2^63 ~2^63 -1
      • 浮点类型
        • float:单精度,尾数可以精确到7位有效数字 4字节
        • double:双精度,精度是float的两倍 8字节
    • 字符型 char
      • 字符常量是用单引号(‘ ’)括起来的单个字符
      • 还允许使用转义字符‘\’来将其后的字符转变为特殊字符型常量
      • UTF-8 是一种变长的编码方式,在互联网上使用最广的一种 Unicode 的实现方式
    • 布尔类型 boolean
      • 用来判断逻辑条件
        • if条件控制语句;
        • while循环控制语句;
        • do-while循环控制语句;
        • for循环控制语句;
      • boolean类型数据只允许取值true和false,无null。
      • java虚拟机没有任何供 boolean值专用的字节码指令,在编译之后都是用 int数据类型来代替:true用1表示,false用0表示 《java虚拟机规范 8版》
    • 自动类型转换:容量小的类型自动转换为容量大的数据类型
      • byte,short,char之间不会相互转换,他们三者在计算时首先转换为int类型
      • boolean类型不能与其它数据类型运算
      • 任何基本数据类型的值和字符串(String)进行连接运算时(+), 基本数据类型的值将自动转化为字符串(String)类型
  • 引用数据类型
    • 类 Class 包含字符串
      • String不是基本数据类型,属于引用数据类型
      • 强制类型转换 :将容量大的数据类型转换为容量小的数据类型。 使用时要加上强制转换符: (),可能造成精度降低或溢出
      • 字符串不能直接转换为基本类型,可以通过基本类型对应的包装类型来转换
        • 如: String a = “43”; int i = Integer.parseInt(a);
      • boolean类型不可以转换为其它的数据类型
      • 进制:所有数字在计算机底层都以二进制形式存在。(计算机组成原理课程
        • 计算机以二进制补码的形式保存所有的整数
          • 正数的原码、反码、补码都相同
          • 负数的补码是其反码+1
        • 目的:简化计算机的基础电路设计,让机器只有加法,没有减法
        • 在这里插入图片描述
    • 接口 interface
    • 数组 [ ]

变量分类-按声明的位置不同来区划

  • 在方法体外,类体内声明的变量称为成员变量。
    • 实例变量(不加 static 修饰)
    • 类变量(以 static 修饰)
  • 方法体内部声明的变量称为局部变量
    • 形参(方法、构造器中定义的变量)
    • 方法局部变量(在方法内定义)
    • 代码块局部变量(在代码块内定义)

除了局部变量形参外,其他类型需显式初始化

2.4、运算符

定义:运算符是一种特殊的符号,用以表示数据的运算、赋值和比较等。
算术运算符

  • 在这里插入图片描述
  • 算术运算符的注意问题
    • 如果对负数取模,可以把模数负号忽略不记,如: 5%-2=1。 但被模数是负数则不可忽略
    • 对于除号“/”,它的整数除和小数除是有区别的:整数之间做除法时,只保留整数部分而舍弃小数部分
    • +”除字符串相加功能外,还能把非字符串转换成字符串
      • System.out.println(“5+5=”+5+5); //打印结果是? 5+5=55

赋值运算符

  • 符号: =
    • 当“=”两侧数据类型不一致时, 可以使用自动类型转换或使用强制类型转换原则进行处理。
    • 支持连续赋值

比较运算符(关系运算符)

  • 在这里插入图片描述

逻辑运算符

  • &—逻辑与 | —逻辑或 ! —逻辑非
  • && —短路与 || —短路或 ^ —逻辑异或
  • 逻辑运算符用于连接布尔型表达式,在Java中不可以写成3<x<6,应该写成x>3 & x<6

位运算符

  • 位运算是直接对整数的二进制进行的运算
  • 使用场景?在这里插入图片描述
    三元运算符
  • (条件表达式)?表达式1:表达式2;为true, 运算后的结果是表达式1;
  • 表达式1和表达式2为同种类型
  • 运算符的优先级
    在这里插入图片描述

2.5、程序流程控制

流程控制语句是用来控制程序中各语句执行顺序的语句,可以把语句组合成能完成一定功能的小逻辑模块
三种基本流程结构
顺序结构

  • 程序从上到下逐行地执行,中间没有任何判断和跳转

分支结构

  • 根据条件,选择性地执行某段代码。条件表达式必须是布尔表达式、布尔变量
  • 有if…else和switch-case两种分支语句
  • 语句块只有一条执行语句时,一对{}可以省略,但建议保留
  • 当多个条件是“互斥”关系时,条件判断语句及执行语句间顺序无所谓;当多个条件是“包含”关系时,“小上大下 / 子上父下
  • switch语句有关规则
    • switch(表达式)中表达式的值必须是下述几种类型之一: byte, short,char, int, 枚举 (jdk 5.0), String (jdk 7.0)
    • case子句中的值必须是常量
  • if 和 switch语句很像,具体什么场景下,应用哪个语句呢?
    • 如果判断的具体数值不多,而且符合byte、 short 、 char、 int、 String、枚举等几种类型。虽然两个语句都可以使用,建议使用swtich语句。因为效率稍高
    • 对区间判断,对结果为boolean类型判断,使用if, if的使用范围更广。也就是说, 使用switch-case的,都可以改写为if-else

循环结构

  • 根据循环条件,重复性的执行某段代码。
  • 有while、 do…while、 for三种循环语句。
    • for循环
      • for (①初始化部分; ②循环条件部分; ④迭代部分){
        ③循环体部分;
      • 执行过程:①-②-③-④-②-③-④-②-③-④-…-②
      • ②循环条件部分为boolean类型表达式,当值为false时,退出循环
      • ④可以有多个变量更新,用逗号分隔
    • while循环
      • ①初始化部分
        while(②循环条件部分){
        ③循环体部分;
        ④迭代部分;
        }
      • 执行过程:①-②-③-④-②-③-④-②-③-④-…-②
      • for循环和while循环可以相互转换
    • do-while循环
      • 语法格式
        ①初始化部分;
        do{
        ③循环体部分
        ④迭代部分
        }while(②循环条件部分);
      • ①-③-④-②-③-④-②-③-④-…②
    • 特殊关键字的使用:break、 continue
      • break语句用于终止某个语句块的执行
      • break语句出现在多层嵌套的语句块中时,可以通过标签指明要终止的是哪一层语句块
      • 在这里插入图片描述
      • continue语句用于跳过其所在循环语句块的一次执行,继续下一次循环
      • return:并非专门用于结束循环的,它的功能是结束一个方法,不管这个return处于多少层循环之内
  • 注: JDK1.5提供了foreach循环,方便的遍历集合、数组元素

3、数组

定义:是多个相同类型数据按一定顺序排列的集合, 并使用一个名字命名, 并通过编号的方式对这些数据进行统一管理

3.1、概述

1、数组本身是引用数据类型,数组中的元素可以是任何数据类型;
2、创建数组对象会在内存中开辟一整块连续的空间,数组名中的引用时这块连续空间的首地址;
3、数组的长度一旦确定,就不能修改

一维数组的使用:声明
type var[] 或 type[] var;

  • 例如:int a[];
    int[] a1;
    double b[];
    String[] c; //引用类型变量数组
    • 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、对泛型编程的了解?

泛型的定义:意味着编写的代码可以被不同类型的对象所重用。

  • 我们提供了泛指的概念,但具体执行的时候却可以有具体的规则来约束,比如我们用的非常多的ArrayList就是个泛型类,ArrayList作为集合可以存放各种元素,如Integer, String,自定义的各种类型等,但在我们使用的时候通过具体的规则来约束,如我们可以约束集合中只存放Integer类型的元素。
    List iniData = new ArrayList<>()

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

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

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

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

// 抽象父类
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);
}
// 子类实现类
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);
    }
}

5、Java面向对象的核心逻辑

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.7、Volatile关键字如何保证内存可见性 阿里面试第21题

Java提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。

作用:

  • 1、保证此变量对所有线程的可见性(即当一条线程修改了这个值,新值对于其他所有线程来说是立即得知的) 原理是:每次访问变量时都会进行一次刷新,因此每次访问都是主存中最新的版本
  • 2、禁止指令重排序优化(volatile修饰的变量相当于生成内存屏障,重排列时不能把后面的指令排到屏障之前)

使用场景:
当且仅当满足以下所有条件时,才应该使用volatile变量

  • 1、对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值;
  • 2、该变量没有包含在具有其他变量的不变式中。

使用建议:

  • 1、在两个或者更多线程需要访问的成员变量上使用 volatile。当要访问的变量已在 synchronized 代码块中,或者为常量时,没必要使用 volatile;
  • 2、由于使用 volatile 屏蔽掉了 JVM中必要的代码优化,所以效率上比较低,因此一定在必要时才使用此关键字。

缺点:

  • 1、无法实现i++原子操作(解决方案:CAS)使用场景:单线程
  • 2、不具备“互斥性”:多个线程能同时读写主存,不能保证变量的“原子性”:(i++不能作为一个整体,分为3个步骤读-改-写),可以使用cas算法保证数据原子性

Volatile/synchronized两者区别:(锁的目标:关注互斥性和可见性)

  • 1、volatile 不会进行加锁操作
    • volatile 变量是一种稍弱的同步机制,在访问 volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此 volatile变量是一种比synchronized 关键字更轻量级的同步机制。
  • 2、volatile 变量作用类似于同步变量读写操作
    • 从内存可见性来说,写入 volatile变量相当于退出同步代码块,而读取 volatile 变量相当于进入同步代码块;
  • 3、volatile不如 synchronized安全。
  • 4、volatile仅能实现变量的修改可见性,并不能保证原子性;synchronized则可以保证变量的修改可见性和原子性,例如: count++ ;

5.8、synchronized(底层是由JVM层面实现)/锁的升级降级?

1、什么是synchronized
synchronized是Java的关键字,是Java的内置特性,在JVM层面实现了对临界资源的同步互斥访问
1、synchronized特点:
synchronized则可以使用在变量、方法(静态方法,同步方法)、和类级别的
修饰代码块时,减少了锁范围,耗时的代码放外面,可以异步调用
2、synchronized实现原理?**(由一对monitorenter/monitorexit指令实现了同步的语义)
Synchronized的语义底层是通过一个monitor(监视器锁)对象来完成的,当monitor被占用时处于锁定状态
线程执行monitorenter指令时尝试获取monitor的所有权:
1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数+1.
3、如果其他线程已经占用monitor,该线程进入阻塞状态,直到monitor的进入数为0,再尝试获取monitor的所有权
线程执行monitorexit指令完成锁的释放
monitor的进入数-1,如果-1后进入数为 0,线程退出monitor;其他被monitor阻塞的线程尝试获取monitor
优化:
0、java6之前,Monitor的实现完全依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作时一个无差别的重量级操作;
1、代码优化:同步代码块、减少锁粒度、读锁并发
2、JVM对此进行了改进,提供三种不同的Monitor实现: 偏置锁、轻量级锁(CAS操作)、重量级锁(自适应自旋、锁粗化、锁消除)
3、后续版本的synchronized进行了较多改进,在低竞争场景中表现可能优于ReentrantLock
锁的升级降级:
当JVM检测到不同的竞争状况时,会自动切换到适合的锁实现,这种切换就是锁的升级、降级:当没有竞争出现时,默认会使用偏斜锁。
JVM会利用CAS操作(compare and swap),在对象头上的Mark Word部分设置线程ID,以表示这个对象偏向于当前线程,所以并不涉
及真正的互斥锁。这样做的假设是基于在很多应用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏斜锁可以降低无竞争开销。
如果有另外的线程试图锁定某个已经被偏斜过的对象,JVM就需要撤销(revoke)偏斜锁,并切换到轻量级锁实现。轻量级锁依赖CAS操作Mark Word来试图获取锁,
如果重试成功,就使用普通的轻量级锁;否则,进一步升级为重量级锁。
降级:锁降级确实是会发生的,当JVM进入安全点(SafePoint)的时候,会检查是否有闲置的Monitor,然后试图进行降级



5.10、reentrantLock实现原理?(可重入锁,底层是由AQS实现)

//在Java5.0之前,协调共享对象的访问时可以使用的机制只有synchronized和volatile。Java5.0后增加了一些新的机制,是当内置锁不适用时,作为一种可选择的高级功能。
ReentrantLock实现了Lock接口,通过这个接口可以实现同步访问,提供了比synchronized关键字更灵活、更广泛、粒度更细的锁操作
特点:
1、无锁的同步机制(非公平锁)
2、加锁和解锁都需要显示写出,实现了Lock接口,注意一定要在后面unlock
3、可实现轮询锁、定时锁、可中断锁特性
4、提供一个condition类,对锁进行更精确的控制
5、默认使用非公平锁,可插队跳过对线程队列的处理
ReentrantLock的内部类Sync继承了AQS,分为公平锁FairSync和非公平锁NonfairSync
1、公平锁:线程获取锁的顺序和调用lock的顺序一样,FIFO;浪费唤醒锁的时间;是否是AQS队列中的头结点
2、非公平锁:线程获取锁的顺序和调用lock的顺序无关,先执行lock方法的锁不一定先获得锁
优点:
1、显示锁可中断,防止死锁,内置锁不可中断,会产生死锁
2、可以实现非公平锁提高程序性能
3、实现其他特性的锁
4、对锁更精细的控制

AQS原理 详解 20190106
数据结构:state、阻塞队列、双链表、线程封装成Node
state实现独占;双向链表、Node.Sharead实现共享
非公平锁(可以控制公平性)
ReentrantLock fairLock = new ReentrantLock(true);//当公平性为真时,会倾向于将锁赋予等待时间最久的线程。公平性是减少线程“饥饿”情况发生的一个办法
建议:只有当你的程序确实有公平性需求时,才有必要指定它
特性锁
可重入:直到state(表示同步状态)为0,其他锁才可以用(线程自己是可以重复获取此锁的(state会累加))
对锁获取粒度的一个概念:当一个线程视图获取一个它已经获取的锁时,这个获取动作就自动成功。
轮询:用tryLock(long timeout, TimeUnit unit)和tryLock() 这两个方法实现
定时:指的是在指定时间内没有获取到锁,就取消阻塞并返回获取锁失败
可中断:lockInterruptibly,防止死锁

锁的更细粒度的使用:
ReentrantReadWriteLock/StampedLock //适用于当数据量较大,并发读多、并发写少的时候


5.11、synchronized/reentrantLock的区别 20181222

条目synchronizedreentrantLock
1、实现层面:synchronized(JVM层面)Lock(JDK层面)
2、响应中断使用synchronized时,等待的线程会一直等待下去,不能够响应中断;Lock可以让等待锁的线程响应中断
3、立即返回:可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间,而synchronized却无法办到;
4、读写锁:Lock可以提高多个线程进行读操作的效率
5、可实现公平锁sychronized天生就是非公平锁** (怎么实现?)Lock可以实现公平锁(fairness)
6、显式获取和释放synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
cas乐观锁:(非阻塞算法)(CAS是整个并发包的基础 Atimic原子类/ReentrantLock/AQS底层都是CAS)
  • 悲观锁:假定并发环境是悲观的,如果发生并发冲突,就会破坏一致性,所以要通过独占锁彻底禁止冲突发生

  • 乐观锁:(锁的粒度小)假定并发环境是乐观的,即,虽然会有并发冲突,但冲突可发现且不会造成损害,所以,可以不加任何保护,等发现并发冲突后再决定放弃操作还是重试。

  • CAS:是一种硬件对并发的支持,用于管理对共享数据的访问。相当于是无锁的非阻塞实现。多数处理器都都实现了一个CAS指令,实现“Compare And Swap”的语义,CAS包含3个操作数:内存值V,预估值A,更新值B,当且仅当V==A,V=B;否则,不做任何操作。

  • CAS原理:通过unsafe类的compareAndSwap(JNI, Java Native Interface)方法实现的,该方法包括四个参数:第一个参数是要修改的对象,第二个参数是对象中要修改变量的偏移量,第三个参数是修改之前的值,第四个参数是预想修改后的值

  • CAS乐观锁的缺点(三大问题:ABA问题、循环时间长开销大和只能保证一个共享变量的原子操作)

  • 1、ABA问题:如果另一个线程修改V值假设原来是A,先修改成B,再修改回成A。当前线程的CAS操作无法分辨当前V值是否发生过变化
    如何解决ABA问题:使用版本号(在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A)

  • 2、不适用于竞争激烈的情形中:并发越高,失败的次数会越多,CAS如果长时间不成功,会极大的增加CPU的开销。因此CAS不适合竞争十分频繁的场景

  • 3、只能保证一个共享变量的原子操作:对多个共享变量操作时,循环CAS就无法保证操作的原子性
    解决方案:多个变量放在一个对象里来进行CAS操作

乐观锁的业务场景及实现方式 20181222
1、CAS自旋volatile变量,是一种很经典的用法(AQS)
2、在数据库产品中,为保证索引的一致性,一个常见的选择是,保证只有一个线程能够排他性地修改一个索引分区,如何在数据库抽象层面实现呢?
可以考虑为索引分区对象添加一个逻辑上的锁,例如,以当前独占的线程ID作为锁的数值,然后通过原子操作设置lock数值,来实现加锁和释放锁

public class AtomicBTreePartition {
    private volatile long lock;
    public void acquireLock(){}
    public void releaseeLock(){}
}

那么在Java代码中,我们怎么实现锁操作呢?
目前Java提供了两种公共API,可以实现这种CAS操作,比如使用java.util.concurrent.atomic.AtomicLongFieldUpdater,它是基于反射机制创建,我们需要保证类型和字段名称正确

 private satic fnal AtomicLongFieldUpdater<AtomicBTreePartition> lockFieldUpdater =AtomicLongFieldUpdater.newUpdater(AtomicBTreePartition.class, "lock");
 private void acquireLock(){
     long t = Thread.currentThread().getId();
     while (!lockFieldUpdater.compareAndSet(this, 0L, t)){
         // 等待一会儿,数据库操作可能比较慢}
 }

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个线程相关


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.字符串的+操作其本质是创建了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对象确定的。

深拷贝和浅拷贝的原理如下图所示:
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?(虚引用)

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

  • 什么是序列化
    序列化:指的是将java对象转换为二进制流的过程
    反序列化:将二进制流恢复成对象的过程
  • 序列化的解决方案:
    java内置的序列化方式:效率较低
    hessian:效率比protocal buffers稍低
    json和xml:应用广泛
  • 序列化的作用:
    将对象通过网络传输到远端
  • 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();//反序列化
  • 序列化的注意事项:(反序列化时的安全问题)
    1、当一个父类实现序列化、子类自动实现序列化、不需要显示实现serializable接口
    2、若该对象的实例变量引用其他对象,序列化该对象也把引用对象进行序列化
    3、声明为static和transient类型的成员数据不能被序列化。因为static代表类的状态,transient代表对象的临时数据
    4、序列化运行时使用一个称为 serialVersionUID 的版本号与每个可序列化类相关联,
    该序列号在反序列化过程中用于验证序列化对象的发送者和接收者是否为该对象加载了与序列化兼容的类

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补

贫血模型与充血模型
见设计模式

举报

相关推荐

0 条评论