首页 文章 精选 留言 我的

精选列表

搜索[整理],共9148篇文章
优秀的个人博客,低调大师

Java基础篇(01):基本数据类型,核心点整理

本文源码:GitHub·点这里 || GitEE·点这里 一、基本类型 1、基本类型 不使用New创建,声明一个非引用传递的变量,且变量的值直接置于堆栈中,大小不随运行环境变化,效率更高。使用new创建的引用对象存储在堆中。 2、基本信息 基本类型包括如下几种:byte、short、int、long、float、double、boolean、char,可以通过相关方法查看范围大小。 public class IntType01 { public static void main(String[] args) { System.out.println("进制位数:"+Integer.SIZE); System.out.println("最小值:"+Integer.MIN_VALUE); System.out.println("最大值:"+Integer.MAX_VALUE); System.out.println("进制位数:"+Double.SIZE); System.out.println("最小值:"+Double.MIN_VALUE); System.out.println("最大值:"+Double.MAX_VALUE); } } 二、案例用法 1、类型转换 自动转换:范围小的数据类型可以自动转换成范围大的数据类型。 强制转换:把一种数据类型转换为另外一种数据类型。 类型提升:表达式运算中有不同的数据类型,类型会自动向范围大的提升。 public class IntType02 { public static void main(String[] args) { // 自动转换 int i = 112 ; long j = i ; System.out.println(j); // 强制转换 double d = 13.14 ; int f = (int)d; System.out.println(f); // 类型提升 long r = i * j ; System.out.println(r); } } 注意:类型转换中最需要关注的问题就是范围大小问题。 2、包装器类型 基本数据类型不符合面向对象思想,从而出现了包装器类型, 并且包装器添加了更多的属性和方法,自动包装功能可以将基本类型转换为包装器类型。Java为每个原始类型都提供了一个封装类,Integer、Double、Long、Boolean、Byte等等。 public class IntType03 { public static void main(String[] args) { Integer int1 = null ; Double dou1 = 13.14 ; Long lon1 = 123L ; } } Integer变量的默认值为null,说明Integer可以区分出未赋值和值为0的区别,好比考试得0分和没参加考试的区别。 3、字符类型 char类型变量是用来储存Unicode编码的字符的,unicode字符集包含汉字。 public class IntType04 { public static void main(String[] args) { char cha1 = '知'; System.out.println(cha1); } } 注意:可能存在特殊生僻字没有包含在unicode编码字符集中。 4、赋值和运算 += 和 = 的区分:short s1=1;s1=s1+1与short s1=1;s1+=1;问题。 public class IntType05 { public static void main(String[] args) { short s1 = 1 ; // s1 = s1 + 1 ; // 变异错误:s1自动向int类型转换 s1 += 1 ; System.out.println(s1); } } +=运算符是java语言规定的,编译器会对它进行识别处理,因此可以正确编译。 5、布尔类型 两个逻辑值: true和false,通常用来表示关系运算的结果。 public class IntType06 { public static void main(String[] args) { // 存在精度损失问题:0.30000000000000004 System.out.println(3*0.1); // true System.out.println(0.3 == 0.3); // false System.out.println(3*0.1 == 0.3); } } 三、Float和Dubble 1、基础概念 这两个类型可能大部分情况下都说不明白关系和区分,首先要理解几个基础概念。 浮点数:在计算机中用以近似表示任意某个实数。具体的说,这个实数由一个整数或定点数乘以某个基数(计算机中通常是2)的整数次幂得到 单精度浮点数:单精度浮点数是用来表示带有小数部分的实数,一般用于科学计算。占用4个字节(32位)存储空间 双精度浮点数:双精度浮点数(double)是计算机使用的一种数据类型,使用64位(8字节)来存储一个浮点数。 2、对比分析 Float基本描述 位数:32 最小值:1.4E-45 最大值:3.4028235E38 Double基本描述 位数:64 最小值:4.9E-324 最大值:1.7976931348623157E308 案例描述 float和double声明和转换相关演示案例。 public class IntType07 { public static void main(String[] args) { // float 声明 float f1 = 12.3f ; // double 声明 double d1 = 13.4 ; // 向下转型,需要强制转换 float f2 = (float) d1 ; System.out.println("f1="+f1+";d1="+d1+";f2="+f2); } } 四、高精度类型 1、BigInteger 支持任意大小的整数运算,且不会再运算过程有任何丢失情况,没有对应的基本类型,运算也会变得相对复杂,运算速度自然也就会下降。 2、BigDecimal 支持任意精度的定点数,通常用来进行精确的货币计算,在公司的日常开发中,这里通常是硬性要求。 public class IntType08 { public static void main(String[] args) { BigDecimal dec1 = new BigDecimal(3.0) ; BigDecimal dec2 = new BigDecimal(2.11) ; // 精确加法运算 BigDecimal res1 = dec1.add(dec2) ; System.out.println(res1); // 精确减法运算,并截取结果 // HALF_UP:四舍五入 BigDecimal res2 = dec1.subtract(dec2); System.out.println(res2.setScale(1, RoundingMode.HALF_UP)); // 精确乘法运算 BigDecimal res3 = dec1.multiply(dec2) ; System.out.println(res3.doubleValue()); // 精确除法运算,并截取结果 // ROUND_DOWN:直接按保留位数截取 BigDecimal res4 = dec1.divide(dec2,2,BigDecimal.ROUND_DOWN); System.out.println(res4); } } 五、源代码地址 GitHub·地址 https://github.com/cicadasmile/java-base-parent GitEE·地址 https://gitee.com/cicadasmile/java-base-parent

优秀的个人博客,低调大师

整理了8个Python中既冷门又实用的技巧

1.print 打印带有颜色的信息 大家知道 Python 中的信息打印函数 print,一般我们会使用它打印一些东西,作为一个简单调试。 但是你知道么,这个 Print 打印出来的字体颜色是可以设置的。 一个小例子 def esc(code=0): return f'\033[{code}m' print(esc('31;1;0') + 'Error:'+esc()+'important') 在控制台或者 Pycharm 运行这段代码之后你会得到结果。 Error:important 其中 Error 是红色加下划线的,important 为默认色 其设置格式为:033[显示方式;前景色;背景色 m 下面可以设置的参数: 说明:前景色 背景色 颜色 --------------------------------------- 30 40 黑色 31 41 红色 32 42 绿色 33 43 黃色 34 44 蓝色 35 45 紫红色 36 46 青蓝色 37 47 白色 显示方式 意义 ------------------------- 0 终端默认设置 1 高亮显示 4 使用下划线 5 闪烁 7 反白显示 8 不可见 例子: \033[1;31;40m <!--1-高亮显示 31-前景色红色 40-背景色黑色--> 2.在 Python 中使用定时器 今天看到一个比较人性化的定时模块 schedule,目前 star 数为 6432,还是非常的受欢迎,这个模块也是秉承这 For Humans 的原则,这里推荐给大家。地址 https://github.com/dbader/schedule 1.通过 pip 即可安装。 pip install schedule 2.使用案例 import schedule import time def job(): print("I'm working...") schedule.every(10).minutes.do(job) schedule.every().hour.do(job) schedule.every().day.at("10:30").do(job) schedule.every().monday.do(job) schedule.every().wednesday.at("13:15").do(job) schedule.every().minute.at(":17").do(job) while True: schedule.run_pending() time.sleep(1) 从单词的字面意思,你就知道这是做什么的。 举个例子: schedule.every().monday.do(job) 这句代码作用就是就是单词意思,定时器会每个周一运行函数 job,怎么样是不是很简单。 3.实现一个进度条 # Python学习交流QQ群:857662006 from time import sleep def progress(percent=0, width=30): left = width * percent // 100 right = width - left print('\r[', '#' * left, ' ' * right, ']', f' {percent:.0f}%', sep='', end='', flush=True) for i in range(101): progress(i) sleep(0.1) 展示效果上面的代码中的 print 有几个有用的参数,sep 的作用是已什么为分隔符,默认是空格,这里设置为空串是为了让每个字符之间更紧凑,end 参数作用是已什么结尾,默认是回车换行符,这里为了实现进度条的效果,同样设置为空串。 还有最后一个参数 flush,该参数的作用主要是刷新, 默认 flush = False,不刷新,print 到 f 中的内容先存到内存中;而当 flush = True 时它会立即把内容刷新并输出。 之前在Python 下载夏目友人帐中提到了的 tqdm 模块,更好的实现一个进度条. 4.优雅的打印嵌套类型的数据 大家应该都有印象,在打印 json 字符串或者字典的时候,打印出的一坨东西根本就没有一个层次关系,这里主要说的就是输出格式的问题。 import json my_mapping = {'a': 23, 'b': 42, 'c': 0xc0ffee} print(json.dumps(my_mapping, indent=4, sort_keys=True)) 大家可以自己试试只用 print 打印 my_mapping,和例子的这种打印方法。 如果我们打印字典组成的列表呢,这个时候使用 json 的 dumps 方法肯定不行的,不过没关系 用标准库的 pprint 方法同样可以实现上面的方法 # Python学习交流QQ群:857662006 import pprint my_mapping = [{'a': 23, 'b': 42, 'c': 0xc0ffee},{'a': 231, 'b': 42, 'c': 0xc0ffee}] pprint.pprint(my_mapping,width=4) 5.功能简单的类使用 namedtuple 和 dataclass 的方式定义 有时候我们想实现一个类似类的功能,但是没有那么复杂的方法需要操作的时候,这个时候就可以考虑下下面两种方法了。 第一个,namedtuple 又称具名元组,带有名字的元组。它作为 Python 标准库 collections 里的一个模块,可以实现一个类似类的一个功能。 from collections import namedtuple # Python学习交流QQ群:857662006 # 以前简单的类可以使用 namedtuple 实现。 Car = namedtuple('Car', 'color mileage') my_car = Car('red', 3812.4) print(my_car.color) print(my_car) 但是呢,所有属性需要提前定义好才能使用,比如想使用my_car.name,你就得把代码改成下面的样子。 from collections import namedtuple # Python学习交流QQ群:857662006 # 以前简单的类可以使用 namedtuple 实现。 Car = namedtuple('Car', 'color mileage name') my_car = Car('red', 3812.4,"Auto") print(my_car.color) print(my_car.name) 使用 namedtuple 的缺点很明显了。 所以现在更优的方案,那就是 Python3.7 加入到标准库的 dataclass。 其实在 3.6 也可以使用不过需要它被作为第三方的库使用了,使用 pip 安装即可。 使用示例如下: from dataclasses import dataclass @dataclass class Car: color: str mileage: float my_car = Car('red', 3812.4) print(my_car.color) print(my_car) 6.f-string 的 !r,!a,!s f-string出现在Python3.6,作为当前最佳的拼接字符串的形式,看下 f-string 的结构 f ' <text> { <expression> <optional !s, !r, or !a> <optional : format specifier> } <text> ... ' 其中'!s' 在表达式上调用str(),'!r' 调用表达式上的repr(),'!a' 调用表达式上的ascii() (1) 默认情况下,f-string将使用str(),但如果包含转换标志,则可以确保它们使用repr () ! class Comedian: def __init__(self, first_name, last_name, age): self.first_name = first_name self.last_name = last_name self.age = age def __str__(self): return f"{self.first_name} {self.last_name} is {self.age}." def __repr__(self): return f"{self.first_name} {self.last_name} is {self.age}. Surprise!" 调用 >>> new_comedian = Comedian("Eric", "Idle", "74") >>> f"{new_comedian}" 'Eric Idle is 74.' >>> f"{new_comedian}" 'Eric Idle is 74.' >>> f"{new_comedian!r}" 'Eric Idle is 74. Surprise!' (2) !a的例子 >>> a = 'some string' >>> f'{a!r}' "'some string'" 等价于 >>> f'{repr(a)}' "'some string'" (3) !d的例子 类似2 pycon2019有人提出的一个展望!d的功能实现:在 Python3.8 中已经实现上述功能,不过不再使用 !d 了改为了 f"{a=}" 的形式,看过这个视频的发现没有 !d 应该很懵逼 7.f-string 里"="的应用 在 Python3.8 里有这样一个功能 a = 5 print(f"{a=}") 打印之后的结果为 a=5 是不是很方便,不用你再使用 f"a={a}" 了。 8.海象运算符:=的使用 a =6 if (b:=a+1)>6: print(b) 赋值的时候同时可以进行运算,和 Go 语言的赋值类似了。 代码的运行顺序,首先计算 a+1 得到值为 7,然后把 7 赋值给 b,到这里代码相当于下面这样了 b =7 if b>6: print(b) 怎么样?是不是简单了不少,不过这个功能 3.8 开始才能用哦。

优秀的个人博客,低调大师

HP EVA8400删除VDISK后数据恢复过程分步整理

【故障描述】 某地法院一台HP EVA8400存储,2组扩展柜,物理磁盘由12个1T FATA磁盘(AG691A 454414-001)和10个300G 15K FC磁盘(AG690A 454411-001)组成,LUN数量不确定,主机环境为WINDOWS,存储法院历史案例审理材料。 因本案多方转手,所以我们也无法直接得知故障原因。 【初检及分析】 1、电话初检,确定得知,数据出现故障后再未重用。通常按HP-EVA的故障可能推断,数据恢复的可靠性较高。 2、EVA主机及扩展柜正常关机,之后将所有硬盘标好位置序号,拿出。在数据成功恢复之前,不再开启EVA 8400控制器。 3、接手磁盘后,按如下链路对磁盘进行连接。 4、进入WINDOWS环境,用WINHEX查看磁盘情况,发现所有磁盘均可正常识别。 5、查看每个磁盘信息,发现300G FC磁盘存在PV HEAD,而1T FATA磁盘上均无PV HEAD。查看300G磁盘中存储的Metadata,发现仅描述了一个RSS组组成的LUN,大小不足2T,成员为所有300G磁盘。而1T FATA磁盘中残留的LUN信息则至少包括5组信息。上述信息表明,极有可能地,某种原因导致删除了1T 磁盘组成的DISK GROUP内所划分的所有VDISK,并UNGROUP了所有1T FATA磁盘。 6、分析1T FATA磁盘上保留的Metadata,大致判断可恢复率较高。 【恢复过程】 1、对所有磁盘做完整镜像,参见《如何对磁盘做完整备份》或本人博客中的其他文章。 2、使用Frombyte recovery for HP-EVA对300G 磁盘所属的LUN进行恢复。 3、因1T磁盘已全部UNGROUP,关于RSS的分配,以及本身的磁盘ID均无法得知。故需进行人工方式分析RSS配置表。通过META信息的对照,以及通过xor信息区的校验验证,得到如下rss组配置表: 3-0 hd6 3-1 hd8 3-2 hd2 3-3 hd9 3-4 hd10 3-5 hd5 2-0 hd0 2-1 hd7 2-2 hd1 2-3 hd11 2-4 hd3 2-5 hd4 4、重组及整合所有LUN的存储分配表。 5、根据存储分配表,及RSS磁盘分配表,对所有LUN进行提取。提取过程中,对不通过的XOR条带进行人工分析,确定离线情况(本例没有掉线磁盘),确定得到最佳重组结论,再通过Frombyte recovery for HP-EVA进行恢复。 【数据恢复结论】 数据100%恢复成功。

优秀的个人博客,低调大师

【监控体系】最全面系统的Zabbix讲解 | 含源码&监控类型整理

主讲人:王鸿杰,云智慧/企业效能部/架构师 讲师简介:云智慧架构师,PHP/PECL 开发组成员,PECL/SeasClick、PECL/SeasLog Maintainer。6 年研发经验,2018 年加入透视宝团队,致力于 APM 产品的架构与研发,专注于服务的性能分析与优化。2021 年加入效能工程团队,为高效而生,立足于使用最少的资源来解决最大的问题,研究各种不同的 bot 技术,让整个团队更高效运转。 公司简介:云智慧集团成立于2009年,是国内领先的全栈智能业务运维解决方案服务商。经过多年自主研发,公司形成了从IT运维、电力运维到IoT运维的产业布局,覆盖ITOM、ITOA、ITSM、DevOps以及IoT几大领域,为金融、政府、运营商、能源、交通、制造等上百家行业的客户,提供了数字化运维体系建设及全生命周期运维管理解决方案。云智慧秉承Make Digital Online的使命,致力于通过先进的产品技术,为企业数字化转型和提升IT运营效率持续赋能。 从本篇内容你能得到: 1. Zabbix是什么、能做到什么 2. Zabbix组件详解、架构和处理工作流程 3. Zabbix各监控项类型的采集实现(SNMP、ICMP、Ping、简单检查等) 4. Zabbix 模板组成结构与实现 5. Zabbix Agent源码结构与实现分析 6. Zabbix 自动发现逻辑的源码结构与实现详解 全栈监控是什么? 监控一切需要监控的东西,只要能够想到,能够用命令实现的都能用来监控。 基础层:主要偏硬件,监控主机和底层资源。比如cpu、 内存、网络吞吐、硬盘I/O、硬盘使用等 中间层:包括nginx、Redis、MQ、MySQL、 Tomcat等 应用层: HTTP访问的吞吐量、响应时间、 返回码、调用链路分析、性能瓶颈, 还包括用户端的监控等 日志:syslog、nginx log、mysql log等 为什么使用Zabbix? 目前开源的解决方案中,Zabbix和Prometheus是使用较多的两个方案,这两个方案的区别和优势是什么? 对于偏传统的行业,云环境较少的场景下,是比较适合于Zabbix的使用,当然Prometheus是未来发展的方向。 Zabbix是什么? Zabbix源码主要组成: frontends:php,主要负责前端的业务操作 Zabbix agent:采集数据 Zabbx server:同步配置,处理数据,分析告警等。 Zabbix处理流程: 主要由四部分组成: Portal-DB-Server-Agent 详细的工作流程如下: 详细流程可查看下图: Zabbix是怎么做的? Zabbix Process 当添加(自动发现)一个 host,并且为 host 添加对应的模板的时候,Portal 会解析出来对应的 item, preprocess,trigger 然后放在对应的 Db 表里面。 Server 的 dbconfig_thread 会从 Db 拿到对应的数据放在共享内存中,然后poller_thread 进程读取到对应的 items,开启与 Agent 建立连接,遍历 items 从 Agent(snmp,简单检查 等) 获取到对应的指标数据 然后将 指标数据给 preprocessing_manager_thread 进程,preprocessing_manager_thread 进程会安排对应的preprocessing_worker 进程对数据进行 preprocess 最后由 dbsyncer_thread 进程将数据放入 Db 中,Portal 界面从 Db 拿到对应的数据信息绘制和显示 dbsyncer_thread 同步数据到DB,遍历数据 并计算 expression 表达式计算指标,最后触发 escalator_thread 和 alerter_thread 做出相应的动作。 对于可监控指标,Agent 自带多种可监控指标 cmd,Agent 只能监控内置的监控指标,可以通过插件拓展 可监控指标。 Zabbix Server dbconfig_thread: 配置同步到共享内存,保证所有进程都可读 poller_thread: 主动监控,解析 items 调用 Agent *trapper_thread: 收集 Agent 上报上来的数据 pinger_thread: 定期 ping 所有监控主机,简单检查 ICMP *alerter_thread: 告警消息处理进程从 IPC 读出需要发动的消息,发出告警 housekeeper_thread: 管家进程,过期数据清理 discoverer_thread: 主机、服务的自动发现 escalator_thread: 根据触发器的值判断是否告警,具体执行什么动作 dbsyncer_thread:同步数据到 db,并计算触发器值也同步到db selfmon_thread: 自监控 preprocessing_worker_thread: 数据预处理进程,数据从 poller 和 trapper 进程过来 Zabbix Agent collector_thread 周期采集基础信息,主要是内存和 CPU,储存于共享内存中 listener_thread 用于接收 server 或者 proxy 分配的的采集任务,被动采采集 active_checks_thread 主动采集上报 指标类型 按进程(处理方式)划分指标类型 Poller 进程(被动) 1. snmp|SNMP代理检查 simple_checks|简单检查(应用层状态检查) calculated|计算监控项 internal|Zabbix内部检查 ssh_checks|SSH检查 telnet_checks|Telnet检查 external|外部检查 aggregate|汇总检查 odbc_checks|ODBC监控 httppoller 进程(被动) http|HTTP 检查 ipmi_poller_thread 进程(被动) ipmi|IPMI检查 icmp pinger 进程(被动) simple_checks|简单检查(icmp 检查) trapper 进程(主动) trapper|捕捉器监控项 log_items|日志文件监控 snmptrap|SNMP traps preprocess 进程(Follow me) dependent_items|相关项目(从属监控项) 按客户端依赖划分指标类型 无依赖 snmp|SNMP代理检查 ipmi|IPMI检查 simple_checks|简单检查 external|外部检查 odbc_checks|ODBC监控 依赖其他监控项 calculated|计算监控项 aggregate|汇总检查 依赖第三方 zabbix_agent|Zabbix代理检查 (Agent) snmptrap|SNMP traps (snmptrap) log_items|日志文件监控 (Agent) ssh_checks|SSH检查 (ssh 秘钥或密码) telnet_checks|Telnet检查 (Telnet Server和 Telnet 密码) trapper|捕捉器监控项 (Zabbix Sender) http|HTTP 检查 (http 接口) IDEA 被动监控对 Zabbix Server 来说会有大的压力 从监控类型来看,Zabbix 更多的是被动监控,主动监控只有 Agent Active、SnmpTrap,Sender,但是由于 Zabbix Agent 的强大,很多场景下主动监控的指标覆盖度完全能跟被动监控硬怼。 所以要根据具体监控场景,主动和被动互相搭配,耗时的指标更加倾向于主动监控。 Zabbix监控项类型梳理 附件: 请您添加:xiaoyuerwise,备注“附件”获取,或从本文底部扫码获取。 Zabbix的自动发现 自动发现是伴随着指标监控来的,当发现能有采集到预设的指标时,这台主机就会被标记为可用,然后触发预设的Action,通知或者自动添加监控模板等。 自动发现分类 简单检查类 Agent 类型 snmp 类型 ICMP (ping) 类型 自动发现流程 配置 IP 段 配置自动发现指标 配置发现动作 Action 发现主机后添加 event escalator_thread 进程处理对应的 event Zabbix的模板结构 模板结构 从属指标 计算指标 Agent源码 附件: 请您添加:xiaoyuerwise,备注“附件”获取,或从本文底部扫码获取。 写在最后 近年来,在AIOps领域极速发展的背景下,IT工具、平台能力、解决方案、AI场景及可用数据集的迫切需求在各行业迸发。基于此,云智慧在2021年8月发布了AIOps社区,旨在树起一面开源旗帜,为各行业客户、用户、研究者和开发者们构建活跃的用户及开发者社区,共同贡献及解决行业难题、促进该领域技术发展。 成立近半年,社区先后开源了数据可视化编排平台-FlyFish、运维管理平台OMP、云服务管理平台-摩尔平台、Hours算法等产品。其中FlyFish斩获中国开源云联盟2021优秀开源项目奖。OMP运维管理平台入选2021 年度 OSC 中国开源项目「最受欢迎项目」榜单。并在业内首次开源了智能运维开源数据集-GAIA数据集,填补了AIOps开源集数据领域的空白。 2021年11月,云智慧正式成为中国开源云联盟成员单位,2022年1月,云智慧入选SegmentFault 思否「2021 中国技术品牌影响力企业榜单」,这一切都代表着开发者和社区对我们的认可,这也更加坚定了云智慧开源战略的决心。 如果您对云智慧AIOps感兴趣,可以通过下方链接了解我们,也可以添加小助手微信,申请加入开发者交流群,可与大咖进行1V1交流! 云智慧AIOps社区:https://www.cloudwise.ai/ Github地址: https://github.com/CloudWise-OpenSource/FlyFish Gitee地址: https://gitee.com/CloudWise/fly-fish

优秀的个人博客,低调大师

熬夜整理的c/c++万字总结(三),值得收藏!

1、位运算 可以使用 C 对变量中的个别位进行操作。您可能对人们想这样做的原因感到奇怪。这种能力有时确实是必须的,或者至少是有用的。C 提供位的逻辑运算符和移位运算符。在以下例子中,我们将使用二进制计数法写出值,以便您可以了解对位发生的操作。在一个实际程序中,您可以使用一般的形式的整数变量或常量。例如不适用 00011001 的形式,而写为 25 或者 031 或者 0x19.在我们的例子中,我们将使用8位数字,从左到右,每位的编号是 7 到 0。 这一定是你需要的电子书资源,全!点击查看! 程序员书籍资源,点击查看! 1.1 位逻辑运算符 4 个位运算符用于整型数据,包括 char。将这些位运算符成为位运算的原因是它们对每位进行操作,而不影响左右两侧的位。请不要将这些运算符与常规的逻辑运算符(&& 、||和!)相混淆,常规的位的逻辑运算符对整个值进行操作。 1.1.1 按位取反~ 一元运算符~将每个 1 变为 0,将每个 0 变为 1,如下面的例子: ~(10011010) 01100101 假设 a 是一个unsigned char,已赋值为 2。在二进制中,2 是00000010.于是 -a 的值为11111101或者 253。请注意该运算符不会改变 a 的值,a 仍为 2。 unsignedchara=2;//00000010 unsignedcharb=~a;//11111101 printf("ret=%d\n",a);//ret=2 printf("ret=%d\n",b);//ret=253 1.1.2 位与(AND): & 二进制运算符 & 通过对两个操作数逐位进行比较产生一个新值。对于每个位,只有两个操作数的对应位都是 1 时结果才 为 1。 (10010011) & (00111101) = (00010001) C 也有一个组合的位与-赋值运算符:&=。下面两个将产生相同的结果: val&=0377 val=val&0377 1.1.3 位或(OR): | 二进制运算符 | 通过对两个操作数逐位进行比较产生一个新值。对于每个位,如果其中任意操作数中对应的位为 1,那么结果位就为 1。 (10010011)| (00111101) = (10111111) C 也有组合位或-赋值运算符: |= val|=0377 val=val|0377 **1.1.4 位异或: ** 二进制运算符^对两个操作数逐位进行比较。对于每个位,如果操作数中的对应位有一个是 1(但不是都是1),那么结果是 1.如果都是 0 或者都是 1,则结果位 0。 (10010011)^ (00111101) = (10101110) C 也有一个组合的位异或 - 赋值运算符: ^= val^=0377 val=val^0377 1.1.5 用法 1.1.5.1 打开位 已知:10011010: 1.将位 2 打开 flag | 10011010 (10011010)|(00000100)=(10011110) 2.将所有位打开 flag | ~flag (10011010)|(01100101)=(11111111) 1.1.5.2 关闭位 flag & ~flag (10011010)&(01100101)=(00000000) 1.1.5.3 转置位 转置(toggling)一个位表示如果该位打开,则关闭该位;如果该位关闭,则打开。您可以使用位异或运算符来转置。其思想是如果 b 是一个位(1或0),那么如果 b 为 1 则 b^1 为 0,如果 b 为 0,则 1^b 为 1。无论 b 的值是 0 还是 1,0^b 为 b。 flag ^ 0xff (10010011)^(11111111)=(01101100) 1.1.5.4 交换两个数不需要临时变量 //a^b=temp; //a^temp=b; //b^temp=a (10010011)^(00100110)=(10110101) (10110101)^(00100110)=10010011 inta=10; intb=30; 1.2 移位运算符 现在让我们了解一下 C 的移位运算符。移位运算符将位向左或向右移动。同样,我们仍将明确地使用二进制形式来说明该机制的工作原理。 1.2.1 左移 << 左移运算符<<将其左侧操作数的值的每位向左移动,移动的位数由其右侧操作数指定。空出来的位用 0 填充,并且丢弃移出左侧操作数末端的位。在下面例子中,每位向左移动两个位置。 (10001010) << 2 = (00101000) 该操作将产生一个新位置,但是不改变其操作数。 1<<1=2; 2<<1=4; 4<<1=8; 8<<2=32 左移一位相当于原值 *2。 1.2.2 右移 >> 右移运算符>>将其左侧的操作数的值每位向右移动,移动的位数由其右侧的操作数指定。丢弃移出左侧操作数有段的位。对于unsigned类型,使用 0 填充左端空出的位。对于有符号类型,结果依赖于机器。空出的位可能用 0 填充,或者使用符号(最左端)位的副本填充。 //有符号值 (10001010)>>2 (00100010)//在某些系统上的结果值 (10001010)>>2 (11100010)//在另一些系统上的结果 //无符号值 (10001010)>>2 (00100010)//所有系统上的结果值 1.2.3 用法:移位运算符 移位运算符能够提供快捷、高效(依赖于硬件)对 2 的幂的乘法和除法。 number << n: number乘以2的n次幂 number >> n: 如果number非负,则用number除以2的n次幂 2、数组 2.1 一维数组 元素类型角度:数组是相同类型的变量的有序集合 内存角度:连续的一大片内存空间 在讨论多维数组之前,我们还需要学习很多关于一维数组的知识。首先让我们学习一个概念。 2.1.1 数组名 考虑下面这些声明: inta; intb[10]; 我们把 a 称作标量,因为它是个单一的值,这个变量是的类型是一个整数。我们把 b 称作数组,因为它是一些值的集合。下标和数名一起使用,用于标识该集合中某个特定的值。例如,b[0] 表示数组 b 的第 1 个值,b[4] 表示第 5 个值。每个值都是一个特定的标量。 那么问题是 b 的类型是什么?它所表示的又是什么?一个合乎逻辑的答案是它表示整个数组,但事实并非如此。在 C中,在几乎所有数组名的表达式中,数组名的值是一个指针常量,也就是数组第一个元素的地址。它的类型取决于数组元素的类型:如果他们是int类型,那么数组名的类型就是“指向 int 的常量指针”;如果它们是其他类型,那么数组名的类型也就是“指向其他类型的常量指针”。 请问:指针和数组是等价的吗? 答案是否定的。数组名在表达式中使用的时候,编译器才会产生一个指针常量。那么数组在什么情况下不能作为指针常量呢?在以下两种场景下: 当数组名作为sizeof操作符的操作数的时候,此时sizeof返回的是整个数组的长度,而不是指针数组指针的长度。 当数组名作为&操作符的操作数的时候,此时返回的是一个指向数组的指针,而不是指向某个数组元素的指针常量。 intarr[10]; //arr=NULL;//arr作为指针常量,不可修改 int*p=arr;//此时arr作为指针常量来使用 printf("sizeof(arr):%d\n",sizeof(arr));//此时sizeof结果为整个数组的长度 printf("&arrtypeis%s\n",typeid(&arr).name());//int(*)[10]而不是int* 2.1.2 下标引用 intarr[]={1,2,3,4,5,6}; *(arr + 3) ,这个表达式是什么意思呢? 首先,我们说数组在表达式中是一个指向整型的指针,所以此表达式表示 arr 指针向后移动了 3 个元素的长度。然后通过间接访问操作符从这个新地址开始获取这个位置的值。这个和下标的引用的执行过程完全相同。所以如下表达式是等同的: *(arr+3) arr[3] 问题1:数组下标可否为负值? 问题2:请阅读如下代码,说出结果: intarr[]={5,3,6,8,2,9}; int*p=arr+2; printf("*p=%d\n",*p); printf("*p=%d\n",p[-1]); 那么是用下标还是指针来操作数组呢?对于大部分人而言,下标的可读性会强一些。 2.1.3 数组和指针 指针和数组并不是相等的。为了说明这个概念,请考虑下面两个声明: inta[10]; int*b; 声明一个数组时,编译器根据声明所指定的元素数量为数组分配内存空间,然后再创建数组名,指向这段空间的起始位置。声明一个指针变量的时候,编译器只为指针本身分配内存空间,并不为任何整型值分配内存空间,指针并未初始化指向任何现有的内存空间。 因此,表达式 *a 是完全合法的,但是表达式 *b 却是非法的。*b 将访问内存中一个不确定的位置,将会导致程序终止。另一方面b++可以通过编译,a++ 却不行,因为a是一个常量值。 2.1.4 作为函数参数的数组名 当一个数组名作为一个参数传递给一个函数的时候发生什么情况呢? 我们现在知道数组名其实就是一个指向数组第 1 个元素的指针,所以很明白此时传递给函数的是一份指针的拷贝。所以函数的形参实际上是一个指针。但是为了使程序员新手容易上手一些,编译器也接受数组形式的函数形参。因此下面两种函数原型是相等的: intprint_array(int*arr); intprint_array(intarr[]); 我们可以使用任何一种声明,但哪一个更准确一些呢?答案是指针。因为实参实际上是个指针,而不是数组。同样 sizeof arr 值是指针的长度,而不是数组的长度。 现在我们清楚了,为什么一维数组中无须写明它的元素数目了,因为形参只是一个指针,并不需要为数组参数分配内存。另一方面,这种方式使得函数无法知道数组的长度。如果函数需要知道数组的长度,它必须显式传递一个长度参数给函数。 2.2 多维数组 如果某个数组的维数不止1个,它就被称为多维数组。接下来的案例讲解以二维数组举例。 voidtest01(){ //二维数组初始化 intarr1[3][3]={ {1,2,3}, {4,5,6}, {7,8,9} }; intarr2[3][3]={1,2,3,4,5,6,7,8,9}; intarr3[][3]={1,2,3,4,5,6,7,8,9}; //打印二维数组 for(inti=0;i<3;i++){ for(intj=0;j<3;j++){ printf("%d",arr1[i][j]); } printf("\n"); } } 2.2.1 数组名 一维数组名的值是一个指针常量,它的类型是“指向元素类型的指针”,它指向数组的第 1 个元素。多维数组也是同理,多维数组的数组名也是指向第一个元素,只不过第一个元素是一个数组。例如: intarr[3][10] 可以理解为这是一个一维数组,包含了 3 个元素,只是每个元素恰好是包含了 10 个元素的数组。arr 就表示指向它的第1个元素的指针,所以 arr 是一个指向了包含了 10 个整型元素的数组的指针。 2.2.2 指向数组的指针(数组指针) 数组指针,它是指针,指向数组的指针。 数组的类型由元素类型和数组大小共同决定:int array[5] 的类型为 int[5]; C 语言可通过 typedef 定义一个数组类型: 定义数组指针有一下三种方式: //方式一 voidtest01(){ //先定义数组类型,再用数组类型定义数组指针 intarr[10]={1,2,3,4,5,6,7,8,9,10}; //有typedef是定义类型,没有则是定义变量,下面代码定义了一个数组类型ArrayType typedefint(ArrayType)[10]; //intArrayType[10];//定义一个数组,数组名为ArrayType ArrayTypemyarr;//等价于intmyarr[10]; ArrayType*pArr=&arr;//定义了一个数组指针pArr,并且指针指向数组arr for(inti=0;i<10;i++){ printf("%d",(*pArr)[i]); } printf("\n"); } //方式二 voidtest02(){ intarr[10]; //定义数组指针类型 typedefint(*ArrayType)[10]; ArrayTypepArr=&arr;//定义了一个数组指针pArr,并且指针指向数组arr for(inti=0;i<10;i++){ (*pArr)[i]=i+1; } for(inti=0;i<10;i++){ printf("%d",(*pArr)[i]); } printf("\n"); } //方式三 voidtest03(){ intarr[10]; int(*pArr)[10]=&arr; for(inti=0;i<10;i++){ (*pArr)[i]=i+1; } for(inti=0;i<10;i++){ printf("%d",(*pArr)[i]); } printf("\n"); } 2.2.3 指针数组(元素为指针) 2.2.3.1 栈区指针数组 //数组做函数函数,退化为指针 voidarray_sort(char**arr,intlen){ for(inti=0;i<len;i++){ for(intj=len-1;j>i;j--){ //比较两个字符串 if(strcmp(arr[j-1],arr[j])>0){ char*temp=arr[j-1]; arr[j-1]=arr[j]; arr[j]=temp; } } } } //打印数组 voidarray_print(char**arr,intlen){ for(inti=0;i<len;i++){ printf("%s\n",arr[i]); } printf("----------------------\n"); } voidtest(){ //主调函数分配内存 //指针数组 char*p[]={"bbb","aaa","ccc","eee","ddd"}; //char**p={"aaa","bbb","ccc","ddd","eee"};//错误 intlen=sizeof(p)/sizeof(char*); //打印数组 array_print(p,len); //对字符串进行排序 array_sort(p,len); //打印数组 array_print(p,len); } 2.2.3.2 堆区指针数组 //分配内存 char**allocate_memory(intn){ if(n<0){ returnNULL; } char**temp=(char**)malloc(sizeof(char*)*n); if(temp==NULL){ returnNULL; } //分别给每一个指针malloc分配内存 for(inti=0;i<n;i++){ temp[i]=malloc(sizeof(char)*30); sprintf(temp[i],"%2d_helloworld!",i+1); } returntemp; } //打印数组 voidarray_print(char**arr,intlen){ for(inti=0;i<len;i++){ printf("%s\n",arr[i]); } printf("----------------------\n"); } //释放内存 voidfree_memory(char**buf,intlen){ if(buf==NULL){ return; } for(inti=0;i<len;i++){ free(buf[i]); buf[i]=NULL; } free(buf); } voidtest(){ intn=10; char**p=allocate_memory(n); //打印数组 array_print(p,n); //释放内存 free_memory(p,n); } 2.2.4二维数组三种参数形式 2.2.4.1 二维数组的线性存储特性 voidPrintArray(int*arr,intlen){ for(inti=0;i<len;i++){ printf("%d",arr[i]); } printf("\n"); } //二维数组的线性存储 voidtest(){ intarr[][3]={ {1,2,3}, {4,5,6}, {7,8,9} }; intarr2[][3]={1,2,3,4,5,6,7,8,9}; intlen=sizeof(arr2)/sizeof(int); //如何证明二维数组是线性的? //通过将数组首地址指针转成Int*类型,那么步长就变成了4,就可以遍历整个数组 int*p=(int*)arr; for(inti=0;i<len;i++){ printf("%d",p[i]); } printf("\n"); PrintArray((int*)arr,len); PrintArray((int*)arr2,len); } 2.2.4.2 二维数组的3种形式参数 //二维数组的第一种形式 voidPrintArray01(intarr[3][3]){ for(inti=0;i<3;i++){ for(intj=0;j<3;j++){ printf("arr[%d][%d]:%d\n",i,j,arr[i][j]); } } } //二维数组的第二种形式 voidPrintArray02(intarr[][3]){ for(inti=0;i<3;i++){ for(intj=0;j<3;j++){ printf("arr[%d][%d]:%d\n",i,j,arr[i][j]); } } } //二维数组的第二种形式 voidPrintArray03(int(*arr)[3]){ for(inti=0;i<3;i++){ for(intj=0;j<3;j++){ printf("arr[%d][%d]:%d\n",i,j,arr[i][j]); } } } voidtest(){ intarr[][3]={ {1,2,3}, {4,5,6}, {7,8,9} }; PrintArray01(arr); PrintArray02(arr); PrintArray03(arr); } 2.3总结 2.3.1 编程提示 源代码的可读性几乎总是比程序的运行时效率更为重要 只要有可能,函数的指针形参都应该声明为 const。 在多维数组的初始值列表中使用完整的多层花括号提高可读性 2.3.2 内容总结 在绝大多数表达式中,数组名的值是指向数组第 1 个元素的指针。这个规则只有两个例外,sizeof 和对数组名&。 指针和数组并不相等。当我们声明一个数组的时候,同时也分配了内存。但是声明指针的时候,只分配容纳指针本身的空间。 当数组名作为函数参数时,实际传递给函数的是一个指向数组第 1 个元素的指针。 我们不单可以创建指向普通变量的指针,也可创建指向数组的指针。

优秀的个人博客,低调大师

熬夜整理的c/c++万字总结(二),值得收藏!

0.为什么使用指针 假如我们定义了 char a=’A’ ,当需要使用 ‘A’ 时,除了直接调用变量 a ,还可以定义 char *p=&a ,调用 a 的地址,即指向 a 的指针 p ,变量 a( char 类型)只占了一个字节,指针本身的大小由可寻址的字长来决定,指针 p 占用 4 个字节。 这一定是你需要的电子书资源,全!点击查看! 程序员书籍资源,点击查看! 但如果要引用的是占用内存空间比较大东西,用指针也还是 4 个字节即可。 使用指针型变量在很多时候占用更小的内存空间。 变量为了表示数据,指针可以更好的传递数据,举个例子: 第一节课是 1 班语文, 2 班数学,第二节课颠倒过来, 1 班要上数学, 2 班要上语文,那么第一节课下课后需要怎样作调整呢?方案一:课间 1 班学生全都去 2 班, 2 班学生全都来 1 班,当然,走的时候要携带上书本、笔纸、零食……场面一片狼藉;方案二:两位老师课间互换教室。 显然,方案二更好一些,方案二类似使用指针传递地址,方案一将内存中的内容重新“复制”了一份,效率比较低。 在数据传递时,如果数据块较大,可以使用指针传递地址而不是实际数据,即提高传输速度,又节省大量内存。 一个数据缓冲区 char buf[100] ,如果其中 buf[0,1] 为命令号, buf[2,3] 为数据类型, buf[4~7] 为该类型的数值,类型为 int ,使用如下语句进行赋值: *(short*)&buf[0]=DataId; *(short*)&buf[2]=DataType; *(int*)&buf[4]=DataValue; 数据转换,利用指针的灵活的类型转换,可以用来做数据类型转换,比较常用于通讯缓冲区的填充。 指针的机制比较简单,其功能可以被集中重新实现成更抽象化的引用数据形式 函数指针,形如: #define PMYFUN (void*)(int,int) ,可以用在大量分支处理的实例当中,如某通讯根据不同的命令号执行不同类型的命令,则可以建立一个函数指针数组,进行散转。 在数据结构中,链表、树、图等大量的应用都离不开指针。 1. 指针强化 1.1 指针是一种数据类型 操作系统将硬件和软件结合起来,给程序员提供的一种对内存使用的抽象,这种抽象机制使得程序使用的是虚拟存储器,而不是直接操作和使用真实存在的物理存储器。所有的虚拟地址形成的集合就是虚拟地址空间。 内存是一个很大的线性的字节数组,每个字节固定由 8 个二进制位组成,每个字节都有唯一的编号,如下图,这是一个 4G 的内存,他一共有 4x1024x1024x1024 = 4294967296 个字节,那么它的地址范围就是 0 ~ 4294967296 ,十六进制表示就是 0x00000000~0xffffffff ,当程序使用的数据载入内存时,都有自己唯一的一个编号,这个编号就是这个数据的地址。指针就是这样形成的。 1.1.1 指针变量 指针是一种数据类型,占用内存空间,用来保存内存地址。 voidtest01(){ int*p1=0x1234; int***p2=0x1111; printf("p1size:%d\n",sizeof(p1)); printf("p2size:%d\n",sizeof(p2)); //指针是变量,指针本身也占内存空间,指针也可以被赋值 inta=10; p1=&a; printf("p1address:%p\n",&p1); printf("p1address:%p\n",p1); printf("aaddress:%p\n",&a); } 1.1.2 野指针和空指针 1.1.2.1 空指针 标准定义了NULL指针,它作为一个特殊的指针变量,表示不指向任何东西。要使一个指针为NULL,可以给它赋值一个零值。为了测试一个指针百年来那个是否为NULL,你可以将它与零值进行比较。 对指针解引用操作可以获得它所指向的值。但从定义上看,NULL指针并未指向任何东西,因为对一个NULL指针因引用是一个非法的操作,在解引用之前,必须确保它不是一个NULL指针。 如果对一个NULL指针间接访问会发生什么呢?结果因编译器而异。 不允许向NULL和非法地址拷贝内存: voidtest(){ char*p=NULL; //给p指向的内存区域拷贝内容 strcpy(p,"1111");//err char*q=0x1122; //给q指向的内存区域拷贝内容 strcpy(q,"2222");//err } 1.1.2.2 野指针 在使用指针时,要避免野指针的出现: 野指针指向一个已删除的对象或未申请访问受限内存区域的指针。与空指针不同,野指针无法通过简单地判断是否为 NULL避免,而只能通过养成良好的编程习惯来尽力减少。对野指针进行操作很容易造成程序错误。 什么情况下会导致野指针? 指针变量未初始化 任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。 指针释放后未置空 有时指针在free或delete后未赋值 NULL,便会使人以为是合法的。别看free和delete的名字(尤其是delete),它们只是把指针所指的内存给释放掉,但并没有把指针本身干掉。此时指针指向的就是“垃圾”内存。释放后的指针应立即将指针置为NULL,防止产生“野指针”。 指针操作超越变量作用域 不要返回指向栈内存的指针或引用,因为栈内存在函数结束时会被释放。 voidtest(){ int*p=0x001;//未初始化 printf("%p\n",p); *p=100; } 操作野指针是非常危险的操作,应该规避野指针的出现: 初始化时置 NULL 指针变量一定要初始化为NULL,因为任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的。 释放时置 NULL 当指针p指向的内存空间释放时,没有设置指针p的值为NULL。delete和free只是把内存空间释放了,但是并没有将指针p的值赋为NULL。通常判断一个指针是否合法,都是使用if语句测试该指针是否为NULL。 1.1.2.3 void*类型指针 void是一种特殊的指针类型,可以用来存放任意对象的地址。一个void指针存放着一个地址,这一点和其他指针类似。不同的是,我们对它到底储存的是什么对象的地址并不了解。 doublea=2.3; intb=5; void*p=&a; cout<<p<<endl;//输出了a的地址 p=&b; cout<<p<<endl;//输出了b的地址 //cout<<*p<<endl;这一行不可以执行,void*指针只可以储存变量地址,不可以直接操作它指向的对象 由于void是空类型,只保存了指针的值,而丢失了类型信息,我们不知道他指向的数据是什么类型的,只指定这个数据在内存中的起始地址,如果想要完整的提取指向的数据,程序员就必须对这个指针做出正确的类型转换,然后再解指针。 1.1.2.4 void*数组和指针 同类型指针变量可以相互赋值,数组不行,只能一个一个元素的赋值或拷贝 数组在内存中是连续存放的,开辟一块连续的内存空间。数组是根据数组的下进行访问的。指针很灵活,它可以指向任意类型的数据。指针的类型说明了它所指向地址空间的内存。 数组所占存储空间的内存:sizeof(数组名) 数组的大小:sizeof(数组名)/sizeof(数据类型),在32位平台下,无论指针的类型是什么,sizeof(指针名)都是 4 ,在 64 位平台下,无论指针的类型是什么,sizeof(指针名)都是 8 。 数组名作为右值的时候,就是第一个元素的地址 intmain(void) { intarr[5]={1,2,3,4,5}; int*p_first=arr; printf("%d",*p_first);//1 return0; } 指向数组元素的指针 支持 递增 递减 运算。p= p+1意思是,让p指向原来指向的内存块的下一个相邻的相同类型的内存块。在数组中相邻内存就是相邻下标元素。 1.1.3 间接访问操作符 通过一个指针访问它所指向的地址的过程叫做间接访问,或者叫解引用指针,这个用于执行间接访问的操作符是*。 注意:对一个int类型指针解引用会产生一个整型值,类似地,对一个float指针解引用会产生了一个float类型的值。 intarr[5]; int*p=*(&arr); intarr1[5][3]arr1=int(*)[3]&arr1 1)在指针声明时,* 号表示所声明的变量为指针 2)在指针使用时,* 号表示操作指针所指向的内存空间 *相当通过地址(指针变量的值)找到指针指向的内存,再操作内存 *放在等号的左边赋值(给内存赋值,写内存) *放在等号的右边取值(从内存中取值,读内存) //解引用 voidtest01(){ //定义指针 int*p=NULL; //指针指向谁,就把谁的地址赋给指针 inta=10; p=&a; *p=20;//*在左边当左值,必须确保内存可写 //*号放右面,从内存中读值 intb=*p; //必须确保内存可写 char*str="helloworld!"; *str='m'; printf("a:%d\n",a); printf("*p:%d\n",*p); printf("b:%d\n",b); } 1.1.4 指针的步长 指针是一种数据类型,是指它指向的内存空间的数据类型。指针所指向的内存空间决定了指针的步长。指针的步长指的是,当指针+1时候,移动多少字节单位。 思考如下问题: inta=0xaabbccdd; unsignedint*p1=&a; unsignedchar*p2=&a; //为什么*p1打印出来正确结果? printf("%x\n",*p1); //为什么*p2没有打印出来正确结果? printf("%x\n",*p2); //为什么p1指针+1加了4字节? printf("p1=%d\n",p1); printf("p1+1=%d\n",p1+1); //为什么p2指针+1加了1字节? printf("p2=%d\n",p2); printf("p2+1=%d\n",p2+1); 1.1.5 函数与指针 1.1.5.1 函数的参数和指针 C语言中,实参传递给形参,是按值传递的,也就是说,函数中的形参是实参的拷贝份,形参和实参只是在值上面一样,而不是同一个内存数据对象。这就意味着:这种数据传递是单向的,即从调用者传递给被调函数,而被调函数无法修改传递的参数达到回传的效果。 voidchange(inta) { a++;//在函数中改变的只是这个函数的局部变量a,而随着函数执行结束,a被销毁。age还是原来的age,纹丝不动。 } intmain(void) { intage=60; change(age); printf("age=%d",age);//age=60 return0; } 有时候我们可以使用函数的返回值来回传数据,在简单的情况下是可以的,但是如果返回值有其它用途(例如返回函数的执行状态量),或者要回传的数据不止一个,返回值就解决不了了。 传递变量的指针可以轻松解决上述问题。 voidchange(int*pa) { (*pa)++;//因为传递的是age的地址,因此pa指向内存数据age。当在函数中对指针pa解地址时, //会直接去内存中找到age这个数据,然后把它增1。 } intmain(void) { intage=160; change(&age); printf("age=%d",age);//age=61 return0; } 比如指针的一个常见的使用例子: #include<stdio.h> #include<stdlib.h> #include<string.h> voidswap(int*,int*); intmain() { inta=5,b=10; printf("a=%d,b=%d\n",a,b); swap(&a,&b); printf("a=%d,b=%d\n",a,b); return0; } voidswap(int*pa,int*pb) { intt=*pa;*pa=*pb;*pb=t; } 在以上的例子中,swap函数的两个形参pa和pb可以接收两个整型变量的地址,并通过间接访问的方式修改了它指向变量的值。在main函数中调用swap时,提供的实参分别为&a,&b,这样就实现了pa=&a,pb=&b的赋值过程,这样在swap函数中就通过pa修改了 a 的值,通过pb修改了 b 的值。因此,如果需要在被调函数中修改主调函数中变量的值,就需要经过以下几个步骤: 定义函数的形参必须为指针类型,以接收主调函数中传来的变量的地址; 调用函数时实参为变量的地址; 在被调函数中使用*间接访问形参指向的内存空间,实现修改主调函数中变量值的功能。 指针作为函数的形参的另一个典型应用是当函数有多个返回值的情形。比如,需要在一个函数中统计一个数组的最大值、最小值和平均值。当然你可以编写三个函数分别完成统计三个值的功能。但比较啰嗦,如: intGetMax(inta[],intn) { intmax=a[0],i; for(i=1;i<n;i++) { if(max<a[i])max=a[i]; } returnmax; } intGetMin(inta[],intn) { intmin=a[0],i; for(i=1;i<n;i++) { if(min>a[i])min=a[i]; } returnmin; } doubleGetAvg(inta[],intn) { doubleavg=0; inti; for(i=0;i<n;i++) { avg+=a[i]; } returnavg/n; } 其实我们完全可以在一个函数中完成这个功能,由于函数只能有一个返回值,可以返回平均值,最大值和最小值可以通过指针类型的形参来进行实现: doubleStat(inta[],intn,int*pmax,int*pmin) { doubleavg=a[0]; inti; *pmax=*pmin=a[0]; for(i=1;i<n;i++) { avg+=a[i]; if(*pmax<a[i])*pmax=a[i]; if(*pmin>a[i])*pmin=a[i]; } returnavg/n; } 1.1.5.2 函数的指针 一个函数总是占用一段连续的内存区域,函数名在表达式中有时也会被转换为该函数所在内存区域的首地址。我们可以把函数的这个首地址赋予一个指针变量,使指针变量指向函数所在的内存区域,然后通过指针变量就可以找到并调用该函数。这种指针就是函数指针。 函数指针的定义形式为: returnType(*pointerName)(paramlist); returnType 为函数返回值类型,pointerNmae 为指针名称,param list 为函数参数列表。参数列表中可以同时给出参数的类型和名称,也可以只给出参数的类型,省略参数的名称,这一点和函数原型非常类似。 用指针来实现对函数的调用: #include<stdio.h> //返回两个数中较大的一个 intmax(inta,intb) { returna>b?a:b; } intmain() { intx,y,maxval; //定义函数指针 int(*pmax)(int,int)=max;//也可以写作int(*pmax)(inta,intb) printf("Inputtwonumbers:"); scanf("%d%d",&x,&y); maxval=(*pmax)(x,y); printf("Maxvalue:%d\n",maxval); return0; } 1.1.5.3 结构体和指针 结构体指针有特殊的语法: -> 符号 如果p是一个结构体指针,则可以使用 p ->【成员】 的方法访问结构体的成员 typedefstruct { charname[31]; intage; floatscore; }Student; intmain(void) { Studentstu={"Bob",19,98.0}; Student*ps=&stu; ps->age=20; ps->score=99.0; printf("name:%sage:%d ",ps->name,ps->age); return0; } 1.2 指针的意义_间接赋值 1.2.1 间接赋值的三大条件 通过指针间接赋值成立的三大条件: 2个变量(一个普通变量一个指针变量、或者一个实参一个形参) 建立关系 通过 * 操作指针指向的内存 voidtest(){ inta=100;//两个变量 int*p=NULL; //建立关系 //指针指向谁,就把谁的地址赋值给指针 p=&a; //通过*操作内存 *p=22; } 1.2.2 如何定义合适的指针变量 voidtest(){ intb; int*q=&b;//0级指针 int**t=&q; int***m=&t; } 1.2.3 间接赋值:从0级指针到1级指针 intfunc1(){return10;} voidfunc2(inta){ a=100; } //指针的意义_间接赋值 voidtest02(){ inta=0; a=func1(); printf("a=%d\n",a); //为什么没有修改? func2(a); printf("a=%d\n",a); } //指针的间接赋值 voidfunc3(int*a){ *a=100; } voidtest03(){ inta=0; a=func1(); printf("a=%d\n",a); //修改 func3(&a); printf("a=%d\n",a); } 1.2.4 间接赋值:从1级指针到2级指针 voidAllocateSpace(char**p){ *p=(char*)malloc(100); strcpy(*p,"helloworld!"); } voidFreeSpace(char**p){ if(p==NULL){ return; } if(*p!=NULL){ free(*p); *p=NULL; } } voidtest(){ char*p=NULL; AllocateSpace(&p); printf("%s\n",p); FreeSpace(&p); if(p==NULL){ printf("p内存释放!\n"); } } 1.2.4 间接赋值的推论 用1级指针形参,去间接修改了0级指针(实参)的值。 用2级指针形参,去间接修改了1级指针(实参)的值。 用3级指针形参,去间接修改了2级指针(实参)的值。 用n级指针形参,去间接修改了n-1级指针(实参)的值。 1.3 指针做函数参数 指针做函数参数,具备输入和输出特性: 输入:主调函数分配内存 输出:被调用函数分配内存 1.3.1 输入特性 voidfun(char*p/*in*/) { //给p指向的内存区域拷贝内容 strcpy(p,"abcddsgsd"); } voidtest(void) { //输入,主调函数分配内存 charbuf[100]={0}; fun(buf); printf("buf=%s\n",buf); } 1.3.2 输出特性 voidfun(char**p/*out*/,int*len) { char*tmp=(char*)malloc(100); if(tmp==NULL) { return; } strcpy(tmp,"adlsgjldsk"); //间接赋值 *p=tmp; *len=strlen(tmp); } voidtest(void) { //输出,被调用函数分配内存,地址传递 char*p=NULL; intlen=0; fun(&p,&len); if(p!=NULL) { printf("p=%s,len=%d\n",p,len); } } 1.4 字符串指针强化 1.4.1 字符串指针做函数参数 1.4.1.1 字符串基本操作 //字符串基本操作 //字符串是以0或者'\0'结尾的字符数组,(数字0和字符'\0'等价) voidtest01(){ //字符数组只能初始化5个字符,当输出的时候,从开始位置直到找到0结束 charstr1[]={'h','e','l','l','o'}; printf("%s\n",str1); //字符数组部分初始化,剩余填0 charstr2[100]={'h','e','l','l','o'}; printf("%s\n",str2); //如果以字符串初始化,那么编译器默认会在字符串尾部添加'\0' charstr3[]="hello"; printf("%s\n",str3); printf("sizeofstr:%d\n",sizeof(str3)); printf("strlenstr:%d\n",strlen(str3)); //sizeof计算数组大小,数组包含'\0'字符 //strlen计算字符串的长度,到'\0'结束 //那么如果我这么写,结果是多少呢? charstr4[100]="hello"; printf("sizeofstr:%d\n",sizeof(str4)); printf("strlenstr:%d\n",strlen(str4)); //请问下面输入结果是多少?sizeof结果是多少?strlen结果是多少? charstr5[]="hello\0world"; printf("%s\n",str5); printf("sizeofstr5:%d\n",sizeof(str5)); printf("strlenstr5:%d\n",strlen(str5)); //再请问下面输入结果是多少?sizeof结果是多少?strlen结果是多少? charstr6[]="hello\012world"; printf("%s\n",str6); printf("sizeofstr6:%d\n",sizeof(str6)); printf("strlenstr6:%d\n",strlen(str6)); } 八进制和十六进制转义字符: 在C中有两种特殊的字符,八进制转义字符和十六进制转义字符,八进制字符的一般形式是'\ddd',d是0-7的数字。十六进制字符的一般形式是'\xhh',h是0-9或A-F内的一个。八进制字符和十六进制字符表示的是字符的ASCII码对应的数值。 比如 : '\063'表示的是字符'3',因为'3'的ASCII码是30(十六进制),48(十进制),63(八进制)。 '\x41'表示的是字符'A',因为'A'的ASCII码是41(十六进制),65(十进制),101(八进制)。 1.4.1.2 字符串拷贝功能实现 //拷贝方法1 voidcopy_string01(char*dest,char*source){ for(inti=0;source[i]!='\0';i++){ dest[i]=source[i]; } } //拷贝方法2 voidcopy_string02(char*dest,char*source){ while(*source!='\0'/**source!=0*/){ *dest=*source; source++; dest++; } } //拷贝方法3 voidcopy_string03(char*dest,char*source){ //判断*dest是否为0,0则退出循环 while(*dest++=*source++){} } 1.4.1.3 字符串反转模型 voidreverse_string(char*str){ if(str==NULL){ return; } intbegin=0; intend=strlen(str)-1; while(begin<end){ //交换两个字符元素 chartemp=str[begin]; str[begin]=str[end]; str[end]=temp; begin++; end--; } } voidtest(){ charstr[]="abcdefghijklmn"; printf("str:%s\n",str); reverse_string(str); printf("str:%s\n",str); } 1.4.2 字符串的格式化 1.4.2.1 sprintf #include<stdio.h> intsprintf(char*str,constchar*format,...); 功能:根据参数format字符串来转换并格式化数据,然后将结果输出到str指定的空间中,直到 出现字符串结束符 '\0' 为止。 参数: str:字符串首地址 format:字符串格式,用法和printf()一样 返回值: 成功:实际格式化的字符个数 失败: - 1 voidtest(){ //1.格式化字符串 charbuf[1024]={0}; sprintf(buf,"你好,%s,欢迎加入我们!","John"); printf("buf:%s\n",buf); memset(buf,0,1024); sprintf(buf,"我今年%d岁了!",20); printf("buf:%s\n",buf); //2.拼接字符串 memset(buf,0,1024); charstr1[]="hello"; charstr2[]="world"; intlen=sprintf(buf,"%s%s",str1,str2); printf("buf:%slen:%d\n",buf,len); //3.数字转字符串 memset(buf,0,1024); intnum=100; sprintf(buf,"%d",num); printf("buf:%s\n",buf); //设置宽度右对齐 memset(buf,0,1024); sprintf(buf,"%8d",num); printf("buf:%s\n",buf); //设置宽度左对齐 memset(buf,0,1024); sprintf(buf,"%-8d",num); printf("buf:%s\n",buf); //转成16进制字符串小写 memset(buf,0,1024); sprintf(buf,"0x%x",num); printf("buf:%s\n",buf); //转成8进制字符串 memset(buf,0,1024); sprintf(buf,"0%o",num); printf("buf:%s\n",buf); } 1.4.2.2 sscanf #include<stdio.h> intsscanf(constchar*str,constchar*format,...); 功能:从str指定的字符串读取数据,并根据参数format字符串来转换并格式化数据。 参数: str:指定的字符串首地址 format:字符串格式,用法和scanf()一样 返回值: 成功:成功则返回参数数目,失败则返回-1 失败: - 1 //1.跳过数据 voidtest01(){ charbuf[1024]={0}; //跳过前面的数字 //匹配第一个字符是否是数字,如果是,则跳过 //如果不是则停止匹配 sscanf("123456aaaa","%*d%s",buf); printf("buf:%s\n",buf); } //2.读取指定宽度数据 voidtest02(){ charbuf[1024]={0}; //跳过前面的数字 sscanf("123456aaaa","%7s",buf); printf("buf:%s\n",buf); } //3.匹配a-z中任意字符 voidtest03(){ charbuf[1024]={0}; //跳过前面的数字 //先匹配第一个字符,判断字符是否是a-z中的字符,如果是匹配 //如果不是停止匹配 sscanf("abcdefg123456","%[a-z]",buf); printf("buf:%s\n",buf); } //4.匹配aBc中的任何一个 voidtest04(){ charbuf[1024]={0}; //跳过前面的数字 //先匹配第一个字符是否是aBc中的一个,如果是,则匹配,如果不是则停止匹配 sscanf("abcdefg123456","%[aBc]",buf); printf("buf:%s\n",buf); } //5.匹配非a的任意字符 voidtest05(){ charbuf[1024]={0}; //跳过前面的数字 //先匹配第一个字符是否是aBc中的一个,如果是,则匹配,如果不是则停止匹配 sscanf("bcdefag123456","%[^a]",buf); printf("buf:%s\n",buf); } //6.匹配非a-z中的任意字符 voidtest06(){ charbuf[1024]={0}; //跳过前面的数字 //先匹配第一个字符是否是aBc中的一个,如果是,则匹配,如果不是则停止匹配 sscanf("123456ABCDbcdefag","%[^a-z]",buf); printf("buf:%s\n",buf); } 1.5 一级指针易错点 1.5.1 越界 voidtest(){ charbuf[3]="abc"; printf("buf:%s\n",buf); } 1.5.2 指针叠加会不断改变指针指向 voidtest(){ char*p=(char*)malloc(50); charbuf[]="abcdef"; intn=strlen(buf); inti=0; for(i=0;i<n;i++) { *p=buf[i]; p++;//修改原指针指向 } free(p); } 1.5.3 返回局部变量地址 char*get_str() { charstr[]="abcdedsgads";//栈区, printf("[get_str]str=%s\n",str); returnstr; } 1.5.4 同一块内存释放多次(不可以释放野指针) voidtest(){ char*p=NULL; p=(char*)malloc(50); strcpy(p,"abcdef"); if(p!=NULL) { //free()函数的功能只是告诉系统p指向的内存可以回收了 //就是说,p指向的内存使用权交还给系统 //但是,p的值还是原来的值(野指针),p还是指向原来的内存 free(p); } if(p!=NULL) { free(p); } } 1.6 const使用 //const修饰变量 voidtest01(){ //1.const基本概念 constinti=0; //i=100;//错误,只读变量初始化之后不能修改 //2.定义const变量最好初始化 constintj; //j=100;//错误,不能再次赋值 //3.c语言的const是一个只读变量,并不是一个常量,可通过指针间接修改 constintk=10; //k=100;//错误,不可直接修改,我们可通过指针间接修改 printf("k:%d\n",k); int*p=&k; *p=100; printf("k:%d\n",k); } //const修饰指针 voidtest02(){ inta=10; intb=20; //const放在*号左侧修饰p_a指针指向的内存空间不能修改,但可修改指针的指向 constint*p_a=&a; //*p_a=100;//不可修改指针指向的内存空间 p_a=&b;//可修改指针的指向 //const放在*号的右侧,修饰指针的指向不能修改,但是可修改指针指向的内存空间 int*constp_b=&a; //p_b=&b;//不可修改指针的指向 *p_b=100;//可修改指针指向的内存空间 //指针的指向和指针指向的内存空间都不能修改 constint*constp_c=&a; } //const指针用法 structPerson{ charname[64]; intid; intage; intscore; }; //每次都对对象进行拷贝,效率低,应该用指针 voidprintPersonByValue(structPersonperson){ printf("Name:%s\n",person.name); printf("Name:%d\n",person.id); printf("Name:%d\n",person.age); printf("Name:%d\n",person.score); } //但是用指针会有副作用,可能会不小心修改原数据 voidprintPersonByPointer(conststructPerson*person){ printf("Name:%s\n",person->name); printf("Name:%d\n",person->id); printf("Name:%d\n",person->age); printf("Name:%d\n",person->score); } voidtest03(){ structPersonp={"Obama",1101,23,87}; //printPersonByValue(p); printPersonByPointer(&p); } 2. 指针的指针(二级指针) 2.1 二级指针基本概念 这里让我们花点时间来看一个例子,揭开这个即将开始的序幕。考虑下面这些声明: inta=12; int*b=&a; 它们如下图进行内存分配: 假定我们又有了第3个变量,名叫c,并用下面这条语句对它进行初始化: c=&b; 它在内存中的大概模样大致如下: c的类型是什么?显然它是一个指针,但它所指向的是什么? 变量b是一个“指向整型的指针”,所以任何指向b的类型必须是指向“指向整型的指针”的指针,更通俗地说,是一个指针的指针。 它合法吗? 是的!指针变量和其他变量一样,占据内存中某个特定的位置,所以用&操作符取得它的地址是合法的。 那么这个变量的声明是怎样的声明的呢? int**c=&b; 那么这个**c如何理解呢?操作符具有从右想做的结合性,所以这个表达式相当于(*c),我们从里向外逐层求职。*c访问c所指向的位置,我们知道这是变量b.第二个间接访问操作符访问这个位置所指向的地址,也就是变量a.指针的指针并不难懂,只需要留心所有的箭头,如果表达式中出现了间接访问操作符,你就要随箭头访问它所指向的位置。 2.2 二级指针做形参输出特性 二级指针做参数的输出特性是指由被调函数分配内存。 //被调函数,由参数n确定分配多少个元素内存 voidallocate_space(int**arr,intn){ //堆上分配n个int类型元素内存 int*temp=(int*)malloc(sizeof(int)*n); if(NULL==temp){ return; } //给内存初始化值 int*pTemp=temp; for(inti=0;i<n;i++){ //temp[i]=i+100; *pTemp=i+100; pTemp++; } //指针间接赋值 *arr=temp; } //打印数组 voidprint_array(int*arr,intn){ for(inti=0;i<n;i++){ printf("%d",arr[i]); } printf("\n"); } //二级指针输出特性(由被调函数分配内存) voidtest(){ int*arr=NULL; intn=10; //给arr指针间接赋值 allocate_space(&arr,n); //输出arr指向数组的内存 print_array(arr,n); //释放arr所指向内存空间的值 if(arr!=NULL){ free(arr); arr=NULL; } } 2.3 二级指针做形参输入特性 二级指针做形参输入特性是指由主调函数分配内存。 //打印数组 voidprint_array(int**arr,intn){ for(inti=0;i<n;i++){ printf("%d",*(arr[i])); } printf("\n"); } //二级指针输入特性(由主调函数分配内存) voidtest(){ inta1=10; inta2=20; inta3=30; inta4=40; inta5=50; intn=5; int**arr=(int**)malloc(sizeof(int*)*n); arr[0]=&a1; arr[1]=&a2; arr[2]=&a3; arr[3]=&a4; arr[4]=&a5; print_array(arr,n); free(arr); arr=NULL; } 2.4 强化训练_画出内存模型图 voidmian() { //栈区指针数组 char*p1[]={"aaaaa","bbbbb","ccccc"}; //堆区指针数组 char**p3=(char**)malloc(3*sizeof(char*));//char*array[3]; inti=0; for(i=0;i<3;i++) { p3[i]=(char*)malloc(10*sizeof(char));//charbuf[10] sprintf(p3[i],"%d%d%d",i,i,i); } } 2.4 多级指针 将堆区数组指针案例改为三级指针案例: //分配内存 voidallocate_memory(char***p,intn){ if(n<0){ return; } char**temp=(char**)malloc(sizeof(char*)*n); if(temp==NULL){ return; } //分别给每一个指针malloc分配内存 for(inti=0;i<n;i++){ temp[i]=malloc(sizeof(char)*30); sprintf(temp[i],"%2d_helloworld!",i+1); } *p=temp; } //打印数组 voidarray_print(char**arr,intlen){ for(inti=0;i<len;i++){ printf("%s\n",arr[i]); } printf("----------------------\n"); } //释放内存 voidfree_memory(char***buf,intlen){ if(buf==NULL){ return; } char**temp=*buf; for(inti=0;i<len;i++){ free(temp[i]); temp[i]=NULL; } free(temp); } voidtest(){ intn=10; char**p=NULL; allocate_memory(&p,n); //打印数组 array_print(p,n); //释放内存 free_memory(&p,n); } 2.5 深拷贝和浅拷贝 如果2个程序单元(例如2个函数)是通过拷贝 他们所共享的数据的 指针来工作的,这就是浅拷贝,因为真正要访问的数据并没有被拷贝。如果被访问的数据被拷贝了,在每个单元中都有自己的一份,对目标数据的操作相互 不受影响,则叫做深拷贝。 #include<iostream> usingnamespacestd; classCopyDemo { public: CopyDemo(intpa,char*cstr)//构造函数,两个参数 { this->a=pa; this->str=newchar[1024];//指针数组,动态的用new在堆上分配存储空间 strcpy(this->str,cstr);//拷贝过来 } //没写,C++会自动帮忙写一个复制构造函数,浅拷贝只复制指针,如下注释部分 //CopyDemo(CopyDemo&obj) //{ //this->a=obj.a; //this->str=obj.str;//这里是浅复制会出问题,要深复制 //} CopyDemo(CopyDemo&obj)//一般数据成员有指针要自己写复制构造函数,如下 { this->a=obj.a; //this->str=obj.str;//这里是浅复制会出问题,要深复制 this->str=newchar[1024];//应该这样写 if(str!=0) strcpy(this->str,obj.str);//如果成功,把内容复制过来 } ~CopyDemo()//析构函数 { deletestr; } public: inta;//定义一个整型的数据成员 char*str;//字符串指针 }; intmain() { CopyDemoA(100,"hello!!!"); CopyDemoB=A;//复制构造函数,把A的10和hello!!!复制给B cout<<"A:"<<A.a<<","<<A.str<<endl; //输出A:100,hello!!! cout<<"B:"<<B.a<<","<<B.str<<endl; //输出B:100,hello!!! //修改后,发现A,B都被改变,原因就是浅复制,A,B指针指向同一地方,修改后都改变 B.a=80; B.str[0]='k'; cout<<"A:"<<A.a<<","<<A.str<<endl; //输出A:100,kello!!! cout<<"B:"<<B.a<<","<<B.str<<endl; //输出B:80,kello!!! return0; } 根据上面实例可以看到,浅复制仅复制对象本身(其中包括是指针的成员),这样不同被复制对象的成员中的对应非空指针会指向同一对象,被成员指针引用的对象成为共享的,无法直接通过指针成员安全地删除(因为若直接删除,另外对象中的指针就会无效,形成所谓的野指针,而访问无效指针是危险的; 除非这些指针有引用计数或者其它手段确保被指对象的所有权);而深复制在浅复制的基础上,连同指针指向的对象也一起复制,代价比较高,但是相对容易管理。 参考资料 C Primer Plus(第五版)中文版 https://www.cnblogs.com/lulipro/p/7460206.html

优秀的个人博客,低调大师

熬夜整理的c/c++万字总结(一),值得收藏!

一. C语言概述 欢迎大家来到c语言的世界,c语言是一种强大的专业化的编程语言。 这一定是你需要的电子书资源,全!点击查看! 程序员书籍资源,点击查看! 1.1 C语言的起源 贝尔实验室的Dennis Ritchie在1972年开发了C,当时他正与ken Thompson一起设计UNIX操作系统,然而,C并不是完全由Ritchie构想出来的。它来自Thompson的B语言。 1.2 使用C语言的理由 在过去的几十年中,c语言已成为最流行和最重要的编程语言之一。它之所以得到发展,是因为人们尝试使用它后都喜欢它。过去很多年中,许多人从c语言转而使用更强大的c++语言,但c有其自身的优势,仍然是一种重要的语言,而且它还是学习c++的必经之路。 高效性。c语言是一种高效的语言。c表现出通常只有汇编语言才具有的精细的控制能力(汇编语言是特定cpu设计所采用的一组内部制定的助记符。不同的cpu类型使用不同的汇编语言)。如果愿意,您可以细调程序以获得最大的速度或最大的内存使用率。 可移植性。c语言是一种可移植的语言。意味着,在一个系统上编写的c程序经过很少改动或不经过修改就可以在其他的系统上运行。 强大的功能和灵活性。c强大而又灵活。比如强大灵活的UNIX操作系统便是用c编写的。其他的语言(Perl、Python、BASIC、Pascal)的许多编译器和解释器也都是用c编写的。结果是当你在一台Unix机器上使用Python时,最终由一个c程序负责生成最后的可执行程序。 1.3 C语言标准 1.3.1 K&R C 起初,C语言没有官方标准。1978年由美国电话电报公司(AT&T)贝尔实验室正式发表了C语言。布莱恩•柯林汉(Brian Kernighan) 和 丹尼斯•里奇(Dennis Ritchie) 出版了一本书,名叫《The C Programming Language》。这本书被 C语言开发者们称为K&R,很多年来被当作 C语言的非正式的标准说明。人们称这个版本的 C语言为K&R C。 K&R C主要介绍了以下特色:结构体(struct)类型;长整数(long int)类型;无符号整数(unsigned int)类型;把运算符=+和=-改为+=和-=。因为=+和=-会使得编译器不知道使用者要处理i = -10还是i =- 10,使得处理上产生混淆。 即使在后来ANSI C标准被提出的许多年后,K&R C仍然是许多编译器的最准要求,许多老旧的编译器仍然运行K&R C的标准。 1.3.2 ANSI C/C89标准 1970到80年代,C语言被广泛应用,从大型主机到小型微机,也衍生了C语言的很多不同版本。1983年,美国国家标准协会(ANSI)成立了一个委员会X3J11,来制定 C语言标准。 1989年,美国国家标准协会(ANSI)通过了C语言标准,被称为ANSI X3.159-1989 "Programming Language C"。因为这个标准是1989年通过的,所以一般简称C89标准。有些人也简称ANSI C,因为这个标准是美国国家标准协会(ANSI)发布的。 1990年,国际标准化组织(ISO)和国际电工委员会(IEC)把C89标准定为C语言的国际标准,命名为ISO/IEC 9899:1990 - Programming languages -- C[5] 。因为此标准是在1990年发布的,所以有些人把简称作C90标准。不过大多数人依然称之为C89标准,因为此标准与ANSI C89标准完全等同。 1994年,国际标准化组织(ISO)和国际电工委员会(IEC)发布了C89标准修订版,名叫ISO/IEC 9899:1990/Cor 1:1994[6] ,有些人简称为C94标准。 1995年,国际标准化组织(ISO)和国际电工委员会(IEC)再次发布了C89标准修订版,名叫ISO/IEC 9899:1990/Amd 1:1995 - C Integrity[7] ,有些人简称为C95标准。 1.3.3 C99标准 1999年1月,国际标准化组织(ISO)和国际电工委员会(IEC)发布了C语言的新标准,名叫ISO/IEC 9899:1999 - Programming languages -- C ,简称C99标准。这是C语言的第二个官方标准。 例如: 增加了新关键字 restrict,inline,_Complex,_Imaginary,_Bool 支持 long long,long double _Complex,float _Complex 这样的类型 支持了不定长的数组。数组的长度就可以用变量了。声明类型的时候呢,就用 int a[*] 这样的写法。不过考虑到效率和实现,这玩意并不是一个新类型。 二、内存分区 2.1 数据类型 2.1.1 数据类型概念 什么是数据类型?为什么需要数据类型? 数据类型是为了更好进行内存的管理,让编译器能确定分配多少内存。 我们现实生活中,狗是狗,鸟是鸟等等,每一种事物都有自己的类型,那么程序中使用数据类型也是来源于生活。 当我们给狗分配内存的时候,也就相当于给狗建造狗窝,给鸟分配内存的时候,也就是给鸟建造一个鸟窝,我们可以给他们各自建造一个别墅,但是会造成内存的浪费,不能很好的利用内存空间。 我们在想,如果给鸟分配内存,只需要鸟窝大小的空间就够了,如果给狗分配内存,那么也只需要狗窝大小的内存,而不是给鸟和狗都分配一座别墅,造成内存的浪费。 当我们定义一个变量,a = 10,编译器如何分配内存?计算机只是一个机器,它怎么知道用多少内存可以放得下10? 所以说,数据类型非常重要,它可以告诉编译器分配多少内存可以放得下我们的数据。 狗窝里面是狗,鸟窝里面是鸟,如果没有数据类型,你怎么知道冰箱里放得是一头大象! 数据类型基本概念: 类型是对数据的抽象; 类型相同的数据具有相同的表示形式、存储格式以及相关操作; 程序中所有的数据都必定属于某种数据类型; 数据类型可以理解为创建变量的模具: 固定大小内存的别名; 2.1.2 数据类型别名 typedefunsignedintu32; typedefstruct_PERSON{ charname[64]; intage; }Person; voidtest(){ u32val;//相当于unsignedintval; Personperson;//相当于structPERSONperson; } 2.1.3 void数据类型 void字面意思是”无类型”,void* 无类型指针,无类型指针可以指向任何类型的数据。 void定义变量是没有任何意义的,当你定义void a,编译器会报错。 void真正用在以下两个方面: 对函数返回的限定; 对函数参数的限定; //1.void修饰函数参数和函数返回 voidtest01(void){ printf("helloworld"); } //2.不能定义void类型变量 voidtest02(){ voidval;//报错 } //3.void*可以指向任何类型的数据,被称为万能指针 voidtest03(){ inta=10; void*p=NULL; p=&a; printf("a:%d\n",*(int*)p); charc='a'; p=&c; printf("c:%c\n",*(char*)p); } //4.void*常用于数据类型的封装 voidtest04(){ //void*memcpy(void*_Dst,constvoid*_Src,size_t_Size); } 2.1.4 sizeof 操作符 sizeof 是 c语言中的一个操作符,类似于++、--等等。sizeof 能够告诉我们编译器为某一特定数据或者某一个类型的数据在内存中分配空间时分配的大小,大小以字节为单位。 基本语法: sizeof(变量); sizeof变量; sizeof(类型); sizeof 注意点: sizeof返回的占用空间大小是为这个变量开辟的大小,而不只是它用到的空间。和现今住房的建筑面积和实用面积的概念差不多。所以对结构体用的时候,大多情况下就得考虑字节对齐的问题了; sizeof返回的数据结果类型是unsigned int; 要注意数组名和指针变量的区别。通常情况下,我们总觉得数组名和指针变量差不多,但是在用sizeof的时候差别很大,对数组名用sizeof返回的是整个数组的大小,而对指针变量进行操作的时候返回的则是指针变量本身所占得空间,在32位机的条件下一般都是4。而且当数组名作为函数参数时,在函数内部,形参也就是个指针,所以不再返回数组的大小; //1.sizeof基本用法 voidtest01(){ inta=10; printf("len:%d\n",sizeof(a)); printf("len:%d\n",sizeof(int)); printf("len:%d\n",sizeofa); } //2.sizeof结果类型 voidtest02(){ unsignedinta=10; if(a-11<0){ printf("结果小于0\n"); } else{ printf("结果大于0\n"); } intb=5; if(sizeof(b)-10<0){ printf("结果小于0\n"); } else{ printf("结果大于0\n"); } } //3.sizeof碰到数组 voidTestArray(intarr[]){ printf("TestArrayarrsize:%d\n",sizeof(arr)); } voidtest03(){ intarr[]={10,20,30,40,50}; printf("arraysize:%d\n",sizeof(arr)); //数组名在某些情况下等价于指针 int*pArr=arr; printf("arr[2]:%d\n",pArr[2]); printf("arraysize:%d\n",sizeof(pArr)); //数组做函数函数参数,将退化为指针,在函数内部不再返回数组大小 TestArray(arr); } 2.1.5 数据类型总结 数据类型本质是固定内存大小的别名,是个模具,C语言规定:通过数据类型定义变量; 数据类型大小计算(sizeof); 可以给已存在的数据类型起别名typedef; 数据类型的封装(void 万能类型); 2.2 变量 2.1.1 变量的概念 既能读又能写的内存对象,称为变量; 若一旦初始化后不能修改的对象则称为常量。 变量定义形式:类型标识符,标识符,…,标识符 2.1.2 变量名的本质 变量名的本质:一段连续内存空间的别名; 程序通过变量来申请和命名内存空间 int a = 0; 通过变量名访问内存空间; 不是向变量名读写数据,而是向变量所代表的内存空间中读写数据; 修改变量的两种方式: voidtest(){ inta=10; //1.直接修改 a=20; printf("直接修改,a:%d\n",a); //2.间接修改 int*p=&a; *p=30; printf("间接修改,a:%d\n",a); } 2.3 程序的内存分区模型 2.3.1 内存分区 2.3.1.1 运行之前 我们要想执行我们编写的c程序,那么第一步需要对这个程序进行编译。 1)预处理:宏定义展开、头文件展开、条件编译,这里并不会检查语法 2)编译:检查语法,将预处理后文件编译生成汇编文件 3)汇编:将汇编文件生成目标文件(二进制文件) 4)链接:将目标文件链接为可执行程序  代码区 存放 CPU 执行的机器指令。通常代码区是可共享的(即另外的执行程序可以调用它),使其可共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可。代码区通常是只读的,使其只读的原因是防止程序意外地修改了它的指t令。另外,代码区还规划了局部变量的相关信息。  全局初始化数据区/静态数据区(data段) 该区包含了在程序中明确被初始化的全局变量、已经初始化的静态变量(包括全局静态变量和t)和常量数据(如字符串常量)。  未初始化数据区(又叫 bss 区) 存入的是全局未初始化变量和未初始化静态变量。未初始化数据区的数据在程序开始执行之前被内核初始化为 0 或者空(NULL)。 总体来讲说,程序源代码被编译之后主要分成两种段:程序指令(代码区)和程序数据(数据区)。代码段属于程序指令,而数据域段和.bss段属于程序数据。 那为什么把程序的指令和程序数据分开呢? 程序被load到内存中之后,可以将数据和代码分别映射到两个内存区域。由于数据区域对进程来说是可读可写的,而指令区域对程序来讲说是只读的,所以分区之后呢,可以将程序指令区域和数据区域分别设置成可读可写或只读。这样可以防止程序的指令有意或者无意被修改; 当系统中运行着多个同样的程序的时候,这些程序执行的指令都是一样的,所以只需要内存中保存一份程序的指令就可以了,只是每一个程序运行中数据不一样而已,这样可以节省大量的内存。比如说之前的Windows Internet Explorer 7.0运行起来之后, 它需要占用112 844KB的内存,它的私有部分数据有大概15 944KB,也就是说有96 900KB空间是共享的,如果程序中运行了几百个这样的进程,可以想象共享的方法可以节省大量的内存。 2.3.1.1 运行之后 程序在加载到内存前,代码区和全局区(data和bss)的大小就是固定的,程序运行期间不能改变。然后,运行可执行程序,操作系统把物理硬盘程序load(加载)到内存,除了根据可执行程序的信息分出代码区(text)、数据区(data)和未初始化数据区(bss)之外,还额外增加了栈区、堆区。  代码区(text segment) 加载的是可执行文件代码段,所有的可执行代码都加载到代码区,这块内存是不可以在运行期间修改的。  未初始化数据区(BSS) 加载的是可执行文件BSS段,位置可以分开亦可以紧靠数据段,存储于数据段的数据(全局未初始化,静态未初始化数据)的生存周期为整个程序运行过程。  全局初始化数据区/静态数据区(data segment) 加载的是可执行文件数据段,存储于数据段(全局初始化,静态初始化数据,文字常量(只读))的数据的生存周期为整个程序运行过程。  栈区(stack) 栈是一种先进后出的内存结构,由编译器自动分配释放,存放函数的参数值、返回值、局部变量等。在程序运行过程中实时加载和释放,因此,局部变量的生存周期为申请到释放该段栈空间。  堆区(heap) 堆是一个大容器,它的容量要远远大于栈,但没有栈那样先进后出的顺序。用于动态内存分配。堆在内存中位于BSS区和栈区之间。一般由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。 2.3.2 分区模型 2.3.2.1 栈区 由系统进行内存的管理。主要存放函数的参数以及局部变量。在函数完成执行,系统自行释放栈区内存,不需要用户管理。 #char*func(){ charp[]="helloworld!";//在栈区存储乱码 printf("%s\n",p); returnp; } voidtest(){ char*p=NULL; p=func(); printf("%s\n",p); } 2.3.2.2 堆区 由编程人员手动申请,手动释放,若不手动释放,程序结束后由系统回收,生命周期是整个程序运行期间。使用malloc或者new进行堆的申请。 char*func(){ char*str=malloc(100); strcpy(str,"helloworld!"); printf("%s\n",str); returnstr; } voidtest01(){ char*p=NULL; p=func(); printf("%s\n",p); } voidallocateSpace(char*p){ p=malloc(100); strcpy(p,"helloworld!"); printf("%s\n",p); } voidtest02(){ char*p=NULL; allocateSpace(p); printf("%s\n",p); } 堆分配内存API: #include<stdlib.h> void*calloc(size_tnmemb,size_tsize); 功能: 在内存动态存储区中分配nmemb块长度为size字节的连续区域。calloc自动将分配的内存 置0。 参数: nmemb:所需内存单元数量 size:每个内存单元的大小(单位:字节) 返回值: 成功:分配空间的起始地址 失败:NULL #include<stdlib.h> void*realloc(void*ptr,size_tsize); 功能: 重新分配用malloc或者calloc函数在堆中分配内存空间的大小。 realloc不会自动清理增加的内存,需要手动清理,如果指定的地址后面有连续的空间,那么就会在已有地址基础上增加内存,如果指定的地址后面没有空间,那么realloc会重新分配新的连续内存,把旧内存的值拷贝到新内存,同时释放旧内存。 参数: ptr:为之前用malloc或者calloc分配的内存地址,如果此参数等于NULL,那么和realloc与malloc功能一致 size:为重新分配内存的大小, 单位:字节 返回值: 成功:新分配的堆内存地址 失败:NULL voidtest01(){ int*p1=calloc(10,sizeof(int)); if(p1==NULL){ return; } for(inti=0;i<10;i++){ p1[i]=i+1; } for(inti=0;i<10;i++){ printf("%d",p1[i]); } printf("\n"); free(p1); } voidtest02(){ int*p1=calloc(10,sizeof(int)); if(p1==NULL){ return; } for(inti=0;i<10;i++){ p1[i]=i+1; } int*p2=realloc(p1,15*sizeof(int)); if(p2==NULL){ return; } printf("%d\n",p1); printf("%d\n",p2); //打印 for(inti=0;i<15;i++){ printf("%d",p2[i]); } printf("\n"); //重新赋值 for(inti=0;i<15;i++){ p2[i]=i+1; } //再次打印 for(inti=0;i<15;i++){ printf("%d",p2[i]); } printf("\n"); free(p2); } 2.3.2.3 全局/静态区 全局静态区内的变量在编译阶段已经分配好内存空间并初始化。这块内存在程序运行期间一直存在,它主要存储全局变量、静态变量和常量。 注意: (1)这里不区分初始化和未初始化的数据区,是因为静态存储区内的变量若不显示初始化,则编译器会自动以默认的方式进行初始化,即静态存储区内不存在未初始化的变量。 (2)全局静态存储区内的常量分为常变量和字符串常量,一经初始化,不可修改。静态存储内的常变量是全局变量,与局部常变量不同,区别在于局部常变量存放于栈,实际可间接通过指针或者引用进行修改,而全局常变量存放于静态常量区则不可以间接修改。 (3)字符串常量存储在全局/静态存储区的常量区。 intv1=10;//全局/静态区 constintv2=20;//常量,一旦初始化,不可修改 staticintv3=20;//全局/静态区 char*p1;//全局/静态区,编译器默认初始化为NULL //那么全局staticint和全局int变量有什么区别? voidtest(){ staticintv4=20;//全局/静态区 } char*func(){ staticchararr[]="helloworld!";//在静态区存储可读可写 arr[2]='c'; char*p="helloworld!";//全局/静态区-字符串常量区 //p[2]='c';//只读,不可修改 printf("%d\n",arr); printf("%d\n",p); printf("%s\n",arr); returnarr; } voidtest(){ char*p=func(); printf("%s\n",p); } 2.3.2.4 总结 在理解C/C++内存分区时,常会碰到如下术语:数据区,堆,栈,静态区,常量区,全局区,字符串常量区,文字常量区,代码区等等,初学者被搞得云里雾里。在这里,尝试捋清楚以上分区的关系。 数据区包括:堆,栈,全局/静态存储区。 全局/静态存储区包括:常量区,全局区、静态区。 常量区包括:字符串常量区、常变量区。 代码区:存放程序编译后的二进制代码,不可寻址区。 可以说,C/C++内存分区其实只有两个,即代码区和数据区。 2.3.3 函数调用模型 2.3.3.1 函数调用流程 栈(stack)是现代计算机程序里最为重要的概念之一,几乎每一个程序都使用了栈,没有栈就没有函数,没有局部变量,也就没有我们如今能见到的所有计算机的语言。在解释为什么栈如此重要之前,我们先了解一下传统的栈的定义: 在经典的计算机科学中,栈被定义为一个特殊的容器,用户可以将数据压入栈中(入栈,push),也可以将压入栈中的数据弹出(出栈,pop),但是栈容器必须遵循一条规则:先入栈的数据最后出栈(First In Last Out,FILO). 在经典的操作系统中,栈总是向下增长的。压栈的操作使得栈顶的地址减小,弹出操作使得栈顶地址增大。 栈在程序运行中具有极其重要的地位。最重要的,栈保存一个函数调用所需要维护的信息,这通常被称为堆栈帧(Stack Frame)或者活动记录(Activate Record).一个函数调用过程所需要的信息一般包括以下几个方面: 函数的返回地址; 函数的参数; 临时变量; 保存的上下文:包括在函数调用前后需要保持不变的寄存器。 我们从下面的代码,分析以下函数的调用过程: intfunc(inta,intb){ intt_a=a; intt_b=b; returnt_a+t_b; } intmain(){ intret=0; ret=func(10,20); returnEXIT_SUCCESS; } 2.3.3.2 调用惯例 现在,我们大致了解了函数调用的过程,这期间有一个现象,那就是函数的调用者和被调用者对函数调用有着一致的理解,例如,它们双方都一致的认为函数的参数是按照某个固定的方式压入栈中。如果不这样的话,函数将无法正确运行。 如果函数调用方在传递参数的时候先压入a参数,再压入b参数,而被调用函数则认为先压入的是b,后压入的是a,那么被调用函数在使用a,b值时候,就会颠倒。 因此,函数的调用方和被调用方对于函数是如何调用的必须有一个明确的约定,只有双方都遵循同样的约定,函数才能够被正确的调用,这样的约定被称为”调用惯例(Calling Convention)”.一个调用惯例一般包含以下几个方面: 函数参数的传递顺序和方式 函数的传递有很多种方式,最常见的是通过栈传递。函数的调用方将参数压入栈中,函数自己再从栈中将参数取出。对于有多个参数的函数,调用惯例要规定函数调用方将参数压栈的顺序:从左向右,还是从右向左。有些调用惯例还允许使用寄存器传递参数,以提高性能。 栈的维护方式 在函数将参数压入栈中之后,函数体会被调用,此后需要将被压入栈中的参数全部弹出,以使得栈在函数调用前后保持一致。这个弹出的工作可以由函数的调用方来完成,也可以由函数本身来完成。 为了在链接的时候对调用惯例进行区分,调用惯例要对函数本身的名字进行修饰。不同的调用惯例有不同的名字修饰策略。 事实上,在c语言里,存在着多个调用惯例,而默认的是cdecl.任何一个没有显示指定调用惯例的函数都是默认是cdecl惯例。比如我们上面对于func函数的声明,它的完整写法应该是: int_cdeclfunc(inta,intb); 注意: cdecl不是标准的关键字,在不同的编译器里可能有不同的写法,例如gcc里就不存在_cdecl这样的关键字,而是使用__attribute_((cdecl)). 2.3.3.2 函数变量传递分析 2.3.4 栈的生长方向和内存存放方向 //1.栈的生长方向 voidtest01(){ inta=10; intb=20; intc=30; intd=40; printf("a=%d\n",&a); printf("b=%d\n",&b); printf("c=%d\n",&c); printf("d=%d\n",&d); //a的地址大于b的地址,故而生长方向向下 } //2.内存生长方向(小端模式) voidtest02(){ //高位字节->地位字节 intnum=0xaabbccdd; unsignedchar*p=&num; //从首地址开始的第一个字节 printf("%x\n",*p); printf("%x\n",*(p+1)); printf("%x\n",*(p+2)); printf("%x\n",*(p+3)); }

优秀的个人博客,低调大师

2021年面试,整理全网初、中、高级常见Java面试题

面试题答案见微信小程序 “Java精选面试题”,3000+道面试题。内容持续更新中包含基础、集合、并发、JVM、Spring、Spring MVC、Spring Boot、Spring Cloud、Dubbo、MySQL、Redis、MyBaits、Zookeeper、Linux、数据结构与算法、项目管理工具、消息队列、设计模式、Nginx、常见 BUG 问题、网络编程等。 ———————————————— 面向对象编程有哪些特征? 一、抽象和封装 类和对象体现了抽象和封装 抽象就是解释类与对象之间关系的词。类与对象之间的关系就是抽象的关系。一句话来说明:类是对象的抽象,而对象则是类得特例,即类的具体表现形式。 封装两个方面的含义:一是将有关数据和操作代码封装在对象当中,形成一个基本单位,各个对象之间相对独立互不干扰。二是将对象中某些属性和操作私有化,已达到数据和操作信息隐蔽,有利于数据安全,防止无关人员修改。把一部分或全部属性和部分功能(函数)对外界屏蔽,就是从外界(类的大括号之外)看不到,不可知,这就是封装的意义。 二、继承 面向对象的继承是为了软件重用,简单理解就是代码复用,把重复使用的代码精简掉的一种手段。如何精简,当一个类中已经有了相应的属性和操作的代码,而另一个类当中也需要写重复的代码,那么就用继承方法,把前面的类当成父类,后面的类当成子类,子类继承父类,理所当然。就用一个关键字extends就完成了代码的复用。 三、多态 没有继承就没有多态,继承是多态的前提。虽然继承自同一父类,但是相应的操作却各不相同,这叫多态。由继承而产生的不同的派生类,其对象对同一消息会做出不同的响应。 JDK、JRE、JVM 之间有什么关系? 1、JDK JDK(Java development Toolkit),JDK是整个Java的核心,包括了Java的运行环境(Java Runtime Environment),一堆的Java工具(Javac,java,jdb等)和Java基础的类库(即Java API 包括rt.jar). Java API 是Java的应用程序的接口,里面有很多写好的Java class,包括一些重要的结构语言以及基本图形,网络和文件I/O等等。 2、JRE JRE(Java Runtime Environment),Java运行环境。在Java平台下,所有的Java程序都需要在JRE下才能运行。只有JVM还不能进行class的执行,因为解释class的时候,JVM需调用解释所需要的类库lib。JRE里面有两个文件夹bin和lib,这里可以认为bin就是JVM,lib就是JVM所需要的类库,而JVM和lib合起来就称JRE。 JRE包括JVM和JAVA核心类库与支持文件。与JDK不同,它不包含开发工具-----编译器,调试器,和其他工具。 3、JVM JVM:Java Virtual Machine(Java 虚拟机)JVM是JRE的一部分,它是虚拟出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM有自己完善的硬件构架,入处理器,堆栈,寄存器等,还有相应的指令系统。 JVM是Java实现跨平台最核心的部分,所有的Java程序会首先被编译为class的类文件,JVM的主要工作是解释自己的指令集(即字节码)并映射到本地的CPU的指令集或OS的系统调用。Java面对不同操作系统使用不同的虚拟机,一次实现了跨平台。JVM对上层的Java源文件是不关心的,它关心的只是由源文件生成的类文件 如何使用命令行编译和运行 Java 文件? 编译和运行Java文件,需了解两个命令: 1)javac命令:编译java文件;使用方法: javac Hello.java ,如果不出错的话,在与Hello.java 同一目录下会生成一个Hello.class文件,这个class文件是操作系统能够使用和运行的文件。 2)java命令: 作用:运行.class文件;使用方法:java Hello,如果不出错的话,会执行Hello.class文件。注意:这里的Hello后面不需要扩展名。 新建文件,编写代码如下: public class Hello{ public static void main(String[] args){ System.out.println("Hello world,欢迎关注微信公众号“Java精选”!"); } } 文件命名为Hello.java,注意后缀为“java”。 打开cmd,切换至当前文件所在位置,执行javac Hello.java,该文件夹下面生成了一个Hello.class文件 输入java Hello命令,cmd控制台打印出代码的内容Hello world,欢迎关注微信公众号“Java精选”! 说说常用的集合有哪些? Map接口和Collection接口是所有集合框架的父接口 Collection 接口的子接口包括:Set接口和List接口。 Set中不能包含重复的元素。List是一个有序的集合,可以包含重复的元素,提供了按索引访问的方式。 Map 接口的实现类主要有:HashMap、Hashtable、ConcurrentHashMap以及TreeMap等。Map不能包含重复的key,但是可以包含相同的value。根据键得到值,对map集合遍历时先得到键的set集合,对set集合进行遍历,得到相应的值。 Set 接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等 List 接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等 Iterator,所有的集合类,都实现了Iterator接口,这是一个用于遍历集合中元素的接口,主要包含以下三种方法: hasNext()是否还有下一个元素 next()返回下一个元素 remove()删除当前元素 进程与线程之间有什么区别? 进程是系统中正在运行的一个程序,程序一旦运行就是进程。 进程可以看成程序执行的一个实例。进程是系统资源分配的独立实体,每个进程都拥有独立的地址空间。一个进程无法访问另一个进程的变量和数据结构,如果想让一个进程访问另一个进程的资源,需要使用进程间通信,比如管道,文件,套接字等。 一个进程可以拥有多个线程,每个线程使用其所属进程的栈空间。线程与进程的一个主要区别是,统一进程内的一个主要区别是,同一进程内的多个线程会共享部分状态,多个线程可以读写同一块内存(一个进程无法直接访问另一进程的内存)。同时,每个线程还拥有自己的寄存器和栈,其他线程可以读写这些栈内存。 线程是进程的一个实体,是进程的一条执行路径。 线程是进程的一个特定执行路径。当一个线程修改了进程的资源,它的兄弟线程可以立即看到这种变化。 什么是 JVM? Java程序的跨平台特性主要是指字节码文件可以在任何具有Java虚拟机的计算机或者电子设备上运行,Java虚拟机中的Java解释器负责将字节码文件解释成为特定的机器码进行运行。 因此在运行时,Java源程序需要通过编译器编译成为.class文件。 众所周知java.exe是java class文件的执行程序,但实际上java.exe程序只是一个执行的外壳,它会装载jvm.dll(windows下,下皆以windows平台为例,linux下和solaris下其实类似,为:libjvm.so),这个动态连接库才是java虚拟机的实际操作处理所在。 JVM是JRE的一部分。它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。 JVM有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java语言最重要的特点就是跨平台运行。 使用JVM就是为了支持与操作系统无关,实现跨平台。所以,JAVA虚拟机JVM是属于JRE的,而现在我们安装JDK时也附带安装了JRE(当然也可以单独安装JRE)。 什么是事务? 事务(transaction)是指数据库操作的最小工作单元,是作为单个逻辑工作单元执行的一系列操作;这些操作作为一个整体一起向系统提交,要么都执行、要么都不执行;事务是一组不可再分割的操作集合(工作逻辑单元)。 通俗的说就是事务可以作为一个单元的一组有序的数据库操作。如果组中的所有操作都成功,则认为事务成功,即使只有一个操作失败,事务也不成功。如果所有操作完成,事务则提交,其修改将作用于所有其他数据库进程。如果一个操作失败,则事务将回滚,该事务所有操作的影响都将取消。 MySQL 事务都有哪些特性? 事务的四大特性: 1 、原子性 事务是数据库的逻辑工作单位,事务中包含的各操作要么都做,要么都不做 2 、一致性 事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。因此当数据库只包含成功事务提交的结果时,就说数据库处于一致性状态。如果数据库系统 运行中发生故障,有些事务尚未完成就被迫中断,这些未完成事务对数据库所做的修改有一部分已写入物理数据库,这时数据库就处于一种不正确的状态,或者说是 不一致的状态。 3 、隔离性 一个事务的执行不能其它事务干扰。即一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能互相干扰。 4 、持续性 也称永久性,指一个事务一旦提交,它对数据库中的数据的改变就应该是永久性的。接下来的其它操作或故障不应该对其执行结果有任何影响。 MyBatis 是什么框架? MyBatis框架是一个优秀的数据持久层框架,在实体类和SQL语句之间建立映射关系,是一种半自动化的ORM实现。其封装性要低于Hibernate,性能优秀,并且小巧。 ORM即对象/关系数据映射,也可以理解为一种数据持久化技术。 MyBatis的基本要素包括核心对象、核心配置文件、SQL映射文件。 数据持久化是将内存中的数据模型转换为存储模型,以及将存储模型转换为内存中的数据模型的统称。 什么是 Redis? redis是一个高性能的key-value数据库,它是完全开源免费的,而且redis是一个NOSQL类型数据库,是为了解决高并发、高扩展,大数据存储等一系列的问题而产生的数据库解决方案,是一个非关系型的数据库。但是,它也是不能替代关系型数据库,只能作为特定环境下的扩充。 redis是一个以key-value存储的数据库结构型服务器,它支持的数据结构类型包括:字符串(String)、链表(lists)、哈希表(hash)、集合(set)、有序集合(Zset)等。为了保证读取的效率,redis把数据对象都存储在内存当中,它可以支持周期性的把更新的数据写入磁盘文件中。而且它还提供了交集和并集,以及一些不同方式排序的操作。 什么是 Spring 框架? Spring中文翻译过来是春天的意思,被称为J2EE的春天,是一个开源的轻量级的Java开发框架, 具有控制反转(IoC)和面向切面(AOP)两大核心。Java Spring框架通过声明式方式灵活地进行事务的管理,提高开发效率和质量。 Spring框架不仅限于服务器端的开发。从简单性、可测试性和松耦合的角度而言,任何 Java 应用都可以从Spring中受益。Spring框架还是一个超级粘合平台,除了自己提供功能外,还提供粘合其他技术和框架的能力。 1)IOC 控制反转 对象创建责任的反转,在Spring中BeanFacotory是IOC容器的核心接口,负责实例化,定位,配置应用程序中的对象及建立这些对象间的依赖。XmlBeanFacotory实现BeanFactory接口,通过获取xml配置文件数据,组成应用对象及对象间的依赖关系。 Spring中有3中注入方式,一种是set注入,另一种是接口注入,另一种是构造方法注入。 2)AOP面向切面编程 AOP是指纵向的编程,比如两个业务,业务1和业务2都需要一个共同的操作,与其往每个业务中都添加同样的代码,通过写一遍代码,让两个业务共同使用这段代码。 Spring中面向切面编程的实现有两种方式,一种是动态代理,一种是CGLIB,动态代理必须要提供接口,而CGLIB实现是由=有继承。 什么是 Spring MVC 框架? Spring MVC属于Spring FrameWork的后续产品,已经融合在Spring Web Flow中。 Spring框架提供了构建Web应用程序的全功能MVC模块。 使用Spring可插入MVC架构,从而在使用Spring进行WEB开发时,可以选择使用Spring中的Spring MVC框架或集成其他MVC开发框架,如Struts1(已基本淘汰),Struts2(老项目还在使用或已重构)等。 通过策略接口,Spring框架是高度可配置的且包含多种视图技术,如JavaServer Pages(JSP)技术、Velocity、Tiles、iText和POI等。 Spring MVC 框架并不清楚或限制使用哪种视图,所以不会强迫开发者只使用JSP技术。 Spring MVC分离了控制器、模型对象、过滤器以及处理程序对象的角色,这种分离让它们更容易进行定制。 什么是 Spring Boot 框架? Spring Boot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程。 Spring Boot框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。通过这种方式,Spring Boot致力于在蓬勃发展的快速应用开发领域(rapid application development)成为领导者。 2014年4月发布第一个版本的全新开源的Spring Boot轻量级框架。它基于Spring4.0设计,不仅继承了Spring框架原有的优秀特性,而且还通过简化配置来进一步简化了Spring应用的整个搭建和开发过程。 另外Spring Boot通过集成大量的框架使得依赖包的版本冲突,以及引用的不稳定性等问题得到了很好的解决。 什么是 Spring Cloud 框架? Spring Cloud是一系列框架的有序集合,它利用Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用Spring Boot的开发风格做到一键启动和部署。 Spring Cloud并没有重复制造轮子,它只是将各家公司开发的比较成熟、经得起实际考验的服务框架组合起来,通过Spring Boot风格进行再封装屏蔽掉了复杂的配置和实现原理,最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包。 Spring Cloud的子项目,大致可分成两大类: 一类是对现有成熟框架“Spring Boot化”的封装和抽象,也是数量最多的项目; 第二类是开发一部分分布式系统的基础设施的实现,如Spring Cloud Stream扮演的是kafka, ActiveMQ这样的角色。 对于快速实践微服务的开发者来说,第一类子项目已经基本足够使用,如: 1)Spring Cloud Netflix是对Netflix开发的一套分布式服务框架的封装,包括服务的发现和注册,负载均衡、断路器、REST客户端、请求路由等; 2)Spring Cloud Config将配置信息中央化保存, 配置Spring Cloud Bus可以实现动态修改配置文件; 3)Spring Cloud Bus分布式消息队列,是对Kafka, MQ的封装; 4)Spring Cloud Security对Spring Security的封装,并能配合Netflix使用; 5)Spring Cloud Zookeeper对Zookeeper的封装,使之能配置其它Spring Cloud的子项目使用; 6)Spring Cloud Eureka是Spring Cloud Netflix微服务套件中的一部分,它基于Netflix Eureka做了二次封装,主要负责完成微服务架构中的服务治理功能。注意的是从2.x起,官方不会继续开源,若需要使用2.x,风险还是有的。但是我觉得问题并不大,eureka目前的功能已经非常稳定,就算不升级,服务注册/发现这些功能已经够用。consul是个不错的替代品,还有其他替代组件,后续篇幅会有详细赘述或关注微信公众号“Java精选”,有详细替代方案源码分享。 Spring Cloud 框架有哪些优缺点? Spring Cloud优点: 1)服务拆分粒度更细,有利于资源重复利用,有利于提高开发效率,每个模块可以独立开发和部署、代码耦合度低; 2)可以更精准的制定优化服务方案,提高系统的可维护性,每个服务可以单独进行部署,升级某个模块的时候只需要单独部署对应的模块服务即可,效率更高; 3)微服务架构采用去中心化思想,服务之间采用Restful等轻量级通讯,比ESB更轻量,模块专一性提升,每个模块只需要关心自己模块所负责的功能即可,不需要关心其他模块业务,专一性更高,更便于功能模块开发和拓展; 4)技术选型不再单一,由于每个模块是单独开发并且部署,所以每个模块可以有更多的技术选型方案,如模块1数据库选择mysql,模块2选择用oracle也是可以的; 5)适于互联网时代,产品迭代周期更短。系统稳定性以及性能提升,由于微服务是几个服务共同组成的项目或者流程,因此相比传统单一项目的优点就在于某个模块提供的服务宕机过后不至于整个系统瘫痪掉,而且微服务里面的容灾和服务降级机制也能大大提高项目的稳定性;从性能而言,由于每个服务是单独部署,所以每个模块都可以有自己的一套运行环境,当某个服务性能低下的时候可以对单个服务进行配置或者代码上的升级,从而达到提升性能的目的。 Spring Cloud缺点: 1)微服务过多,治理成本高,不利于维护系统,服务之间接口调用成本增加,相比以往单项目的时候调用某个方法或者接口可以直接通过本地方法调用就能够完成,但是当切换成微服务的时候,调用方式就不能用以前的方式进行调试、目前主流采用的技术有http api接口调用、RPC、WebService等方式进行调用,调用成本比单个项目的时候有所增加; 2)分布式系统开发的成本高(容错,分布式事务等)对团队挑战大 2)独立的数据库,微服务产生事务一致性的问题,由于各个模块用的技术都各不相同、而且每个服务都会高并发进行调用,就会存在分布式事务一致性的问题; 3)分布式部署,造成运营的成本增加、相比较单个应用的时候,运营人员只需要对单个项目进行部署、负载均衡等操作,但是微服务的每个模块都需要这样的操作,增加了运行时的成本; 4)由于整个系统是通过各个模块组合而成的,因此当某个服务进行变更时需要对前后涉及的所有功能进行回归测试,测试功能不能仅限于当个模块,增加了测试难度和测试成本; 总体来说优点大过于缺点,目前看来Spring Cloud是一套非常完善的微服务框架,目前很多企业开始用微服务,Spring Cloud的优势是显而易见的。 什么是消息队列? MQ全称为Message Queue 消息队列(MQ)是一种应用程序对应用程序的通信方法。 消息队列中间件是分布式系统中重要的组件,主要解决应用耦合,异步消息,流量削锋等问题。实现高性能,高可用,可伸缩和最终一致性架构。是大型分布式系统不可缺少的中间件。 MQ是消费生产者模型的一个典型的代表,一端往消息队列中不断写入消息,而另一端则可以读取队列中的消息。 消息生产者只需要把消息发布到MQ中而不用管谁来获取,消息消费者只管从MQ中获取消息而不管是谁发布的消息,这样生产者和消费者双方都不用清楚对方的存在。 目前在生产环境,使用较多的消息队列有ActiveMQ,RabbitMQ,ZeroMQ,Kafka,MetaMQ,RocketMQ等。 消息队列有哪些应用场景? 列举消息队列场景,异步处理,应用解耦,流量削锋,日志处理和消息通讯五个场景。 1、异步处理场景 用户注册后,需要发注册邮件和注册短信。传统的做法有两种 1)串行方式:将注册信息写入数据库成功后,发送注册邮件,再发送注册短信。以上三个任务全部完成后,返回给客户端。 2)并行方式:将注册信息写入数据库成功后,发送注册邮件的同时,发送注册短信。以上三个任务完成后,返回给客户端。与串行的差别是,并行的方式可以提高处理的时间。 按照以上描述,用户的响应时间相当于是注册信息写入数据库的时间,也就是50毫秒。注册邮件,发送短信写入消息队列后,直接返回,因此写入消息队列的速度很快,基本可以忽略,因此用户的响应时间可能是50毫秒。因此采用消息队列的方式,系统的吞吐量提高到每秒20 QPS。比串行提高了3倍,比并行提高了两倍。 2、应用解耦场景 用户购买物品下单后,订单系统需要通知库存系统。传统的做法是订单系统调用库存系统的接口。该模式的缺点: 1)假如库存系统无法访问,则订单减库存将失败,从而导致订单失败; 2)订单系统与库存系统耦合; 如何解决以上问题呢? 订单系统:用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户订单下单成功。 库存系统:订阅下单的消息,采用拉/推的方式,获取下单信息,库存系统根据下单信息,进行库存操作。 假如:在下单时库存系统不能正常使用。也不影响正常下单,因为下单后订单系统写入消息队列就不再关心其他的后续操作,从而实现订单系统与库存系统的应用解耦。 3、流量削锋场景 流量削锋也是消息队列中的常用场景,一般在秒杀或团抢活动中被广泛应用。 秒杀活动,一般会因为流量并发过大,导致流量暴增,应用宕机,从而为解决该问题,一般在应用前端加入消息队列。 控制活动的人数,缓解短时间内高流量压垮应用。当用户的请求,服务器接收后,先写入消息队列。如消息队列长度超过最大数量,则直接抛弃用户请求或跳转到错误页面,而其秒杀业务根据消息队列中的请求信息,再做后续处理。 4、日志处理场景 日志处理是指将消息队列用在日志处理中,比如Kafka的应用,解决大量日志传输的问题。日志采集客户端,负责日志数据采集,定时写受写入Kafka队列,而Kafka消息队列,负责日志数据的接收,存储和转发,日志处理应用订阅并消费kafka队列中的日志数据。 5、消息通讯 消息通讯是指消息队列一般都内置了高效的通信机制,因此也可以用在纯的消息通讯。比如实现点对点消息队列,或者聊天室等。 什么是 Linux 操作系统? Linux全称GNU/Linux,是一套免费使用和自由传播的类Unix操作系统,是一个基于POSIX的多用户、多任务、支持多线程和多CPU的操作系统。 伴随着互联网的发展,Linux得到了来自全世界软件爱好者、组织、公司的支持。它除了在服务器方面保持着强劲的发展势头以外,在个人电脑、嵌入式系统上都有着长足的进步。使用者不仅可以直观地获取该操作系统的实现机制,而且可以根据自身的需要来修改完善Linux,使其最大化地适应用户的需要。 Linux不仅系统性能稳定,而且是开源软件。其核心防火墙组件性能高效、配置简单,保证了系统的安全。 在很多企业网络中,为了追求速度和安全,Linux不仅仅是被网络运维人员当作服务器使用,甚至当作网络防火墙,这是Linux的一大亮点。 Linux具有开放源码、没有版权、技术社区用户多等特点,开放源码使得用户可以自由裁剪,灵活性高,功能强大,成本低。尤其系统中内嵌网络协议栈,经过适当的配置就可实现路由器的功能。这些特点使得Linux成为开发路由交换设备的理想开发平台。 什么是数据结构? 数据结构是计算机存储、组织数据的方式。数据结构是指相互之间存在一种或多种特定关系的数据元素的集合。通常情况下,精心选择的数据结构可以带来更高的运行或者存储效率。数据结构往往同高效的检索算法和索引技术有关。 数据结构(data structure)是带有结构特性的数据元素的集合,它研究的是数据的逻辑结构和数据的物理结构以及它们之间的相互关系,并对这种结构定义相适应的运算,设计出相应的算法,并确保经过这些运算以后所得到的新结构仍保持原来的结构类型。 简而言之,数据结构是相互之间存在一种或多种特定关系的数据元素的集合,即带“结构”的数据元素的集合。“结构”就是指数据元素之间存在的关系,分为逻辑结构和存储结构。 数据的逻辑结构和物理结构是数据结构的两个密切相关的方面,同一逻辑结构可以对应不同的存储结构。算法的设计取决于数据的逻辑结构,而算法的实现依赖于指定的存储结构。 数据结构的研究内容是构造复杂软件系统的基础,它的核心技术是分解与抽象。 通过分解可以划分出数据的3个层次;再通过抽象,舍弃数据元素的具体内容,就得到逻辑结构。类似地,通过分解将处理要求划分成各种功能,再通过抽象舍弃实现细节,就得到运算的定义。 上述两个方面的结合可以将问题变换为数据结构。这是一个从具体(即具体问题)到抽象(即数据结构)的过程。 然后,通过增加对实现细节的考虑进一步得到存储结构和实现运算,从而完成设计任务。这是一个从抽象(即数据结构)到具体(即具体实现)的过程。 什么是设计模式? 设计模式(Design pattern) 是解决软件开发某些特定问题而提出的一些解决方案也可以理解成解决问题的一些思路,通过设计模式可以帮助我们增强代码的可重用性、可扩充性、 可维护性、灵活性好。使用设计模式最终的目的是实现代码的高内聚和低耦合。 高内聚低耦合是软件工程中的概念,是判断软件设计好坏的标准,主要用于程序的面向对象的设计,主要看类的内聚性是否高,耦合度是否低。 目的是使程序模块的可重用性、移植性大大增强。 通常程序结构中各模块的内聚程度越高,模块间的耦合程度就越低。 内聚是从功能角度来度量模块内的联系,一个好的内聚模块应当恰好做一件事,它描述的是模块内的功能联系;耦合是软件结构中各模块之间相互连接的一种度量,耦合强弱取决于模块间接口的复杂程度、进入或访问一个模块的点以及通过接口的数据。 什么是 Zookeeper? ZooKeeper由雅虎研究院开发,ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现,后来托管到Apache,是Hadoop和Hbase的重要组件。 ZooKeeper是一个经典的分布式数据一致性解决方案,致力于为分布式应用提供一个高性能、高可用,且具有严格顺序访问控制能力的分布式协调服务。 ZooKeeper的目标就是封装好复杂易出错的关键服务,将简单易用的接口和性能高效、功能稳定的系统提供给用户。 ZooKeeper包含一个简单的原语集,提供Java和C的接口。 ZooKeeper代码版本中,提供了分布式独享锁、选举、队列的接口,代码在$zookeeper_home\src\recipes。其中分布锁和队列有Java和C两个版本,选举只有Java版本。 于2010年11月正式成为Apache的顶级项目。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。 分布式应用程序可以基于ZooKeeper实现数据发布与订阅、负载均衡、命名服务、分布式协调与通知、集群管理、Leader选举、分布式锁、分布式队列等功能。 应用服务 8080 端口被意外占用如何解决? 1)按键盘WIN+R键,打开后在运行框中输入“CMD”命令,点击确定。 2)在CMD窗口,输入“netstat -ano”命令,按回车键,即可查看所有的端口占用情况。 3)找到本地地址一览中类似“0.0.0.0:8080”信息,通过此列查看8080端口对应的程序PID。 4)打开任务管理器,详细信息找到对应的应用PID(若不存在通过设置可以调出来),右键结束任务即可。 什么是 Dubbo 框架? Dubbo(读音[ˈdʌbəʊ])是阿里巴巴公司开源的一个高性能优秀的服务框架,使得应用可通过高性能的 RPC 实现服务的输出和输入功能,可以和Spring框架无缝集成。 Dubbo提供了六大核心能力:面向接口代理的高性能RPC调用,智能容错和负载均衡,服务自动注册和发现,高度可扩展能力,运行期流量调度,可视化的服务治理与运维。 Dubbo是一款高性能、轻量级的开源Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。 核心组件 Remoting: 网络通信框架,实现了 sync-over-async 和request-response 消息机制; RPC: 一个远程过程调用的抽象,支持负载均衡、容灾和集群功能; Registry: 服务目录框架用于服务的注册和服务事件发布和订阅。 什么是 Maven? Maven即为项目对象模型(POM),它可以通过一小段描述信息来管理项目的构建,报告和文档的项目管理工具软件。 Maven 除了以程序构建能力为特色之外,还提供高级项目管理工具。 由于Maven的缺省构建规则有较高的可重用性,所以常常用两三行Maven构建脚本就可以构建简单的项目。 由于Maven面向项目的方法,许多Apache Jakarta项目发文时使用Maven,而且公司项目采用Maven的比例在持续增长,相比较Gradle,在之后的篇幅中会说明,欢迎大家关注微信公众号“Java精选”。 Maven这个单词来自于意第绪语(犹太语),意为知识的积累,最初在Jakata Turbine项目中用来简化构建过程。 当时有一些项目(有各自Ant build文件),仅有细微的差别,而JAR文件都由CVS来维护。于是希望有一种标准化的方式构建项目,一个清晰的方式定义项目的组成,一个容易的方式发布项目的信息,以及一种简单的方式在多个项目中共享JARs。 应用层中常见的协议都有哪些? 应用层协议(application layer protocol)定义了运行在不同端系统上的应用程序进程如何相互传递报文。 应用层协议 1)DNS:一种用以将域名转换为IP地址的Internet服务,域名系统DNS是因特网使用的命名系统,用来把便于人们使用的机器名字转换为IP地址。 现在顶级域名TLD分为三大类:国家顶级域名nTLD;通用顶级域名gTLD;基础结构域名。 域名服务器分为四种类型:根域名服务器;顶级域名服务器;本地域名服务器;权限域名服务器。 2)FTP:文件传输协议FTP是因特网上使用得最广泛的文件传送协议。FTP提供交互式的访问,允许客户指明文件类型与格式,并允许文件具有存取权限。 基于客户服务器模式,FTP协议包括两个组成部分,一是FTP服务器,二是FTP客户端,提供交互式的访问面向连接,使用TCP/IP可靠的运输服务,主要功能:减少/消除不同操作系统下文件的不兼容性 。 3)telnet远程终端协议:telnet是一个简单的远程终端协议,它也是因特网的正式标准。又称为终端仿真协议。 4)HTTP:超文本传送协议,是面向事务的应用层协议,它是万维网上能够可靠地交换文件的重要基础。http使用面向连接的TCP作为运输层协议,保证了数据的可靠传输。 5)电子邮件协议SMTP:即简单邮件传送协议。SMTP规定了在两个相互通信的SMTP进程之间应如何交换信息。SMTP通信的三个阶段:建立连接、邮件传送、连接释放。 6)POP3:邮件读取协议,POP3(Post Office Protocol 3)协议通常被用来接收电子邮件。 7)远程登录协议(Telnet):用于实现远程登录功能。 8)SNMP:简单网络管理协议。由三部分组成:SNMP本身、管理信息结构SMI和管理信息MIB。SNMP定义了管理站和代理之间所交换的分组格式。SMI定义了命名对象类型的通用规则,以及把对象和对象的值进行编码。MIB在被管理的实体中创建了命名对象,并规定类型。 Java 中的关键字都有哪些? 1)48个关键字:abstract、assert、boolean、break、byte、case、catch、char、class、continue、default、do、double、else、enum、extends、final、finally、float、for、if、implements、import、int、interface、instanceof、long、native、new、package、private、protected、public、return、short、static、strictfp、super、switch、synchronized、this、throw、throws、transient、try、void、volatile、while。 2)2个保留字(目前未使用,以后可能用作为关键字):goto、const。 3)3个特殊直接量(直接量是指在程序中通过源代码直接给出的值):true、false、null。 Java 中基本类型都有哪些? Java的类型分成两种,一种是基本类型,一种是引用类型。其中Java基本类型共有八种。 基本类型可以分为三大类:字符类型char,布尔类型boolean以及数值类型byte、short、int、long、float、double。 数值类型可以分为整数类型byte、short、int、long和浮点数类型float、double。 JAVA中的数值类型不存在无符号的,它们的取值范围是固定的,不会随着机器硬件环境或操作系统的改变而改变。实际上《Thinking in Java》一书作者,提到Java中还存在另外一种基本类型void,它也有对应的包装类 java.lang.Void,因为Void是不能new,也就是不能在堆里面分配空间存对应的值,所以将Void归成基本类型,也有一定的道理。 8种基本类型表示范围如下: byte:8位,最大存储数据量是255,存放的数据范围是-128~127之间。 short:16位,最大数据存储量是65536,数据范围是-32768~32767之间。 int:32位,最大数据存储容量是2的32次方减1,数据范围是负的2的31次方到正的2的31次方减1。 long:64位,最大数据存储容量是2的64次方减1,数据范围为负的2的63次方到正的2的63次方减1。 float:32位,数据范围在3.4e-45~1.4e38,直接赋值时必须在数字后加上f或F。 double:64位,数据范围在4.9e-324~1.8e308,赋值时可以加d或D也可以不加。 boolean:只有true和false两个取值。 char:16位,存储Unicode码,用单引号赋值。 为什么 Map 接口不继承 Collection 接口? 1)Map提供的是键值对映射(即Key和value的映射),而Collection提供的是一组数据并不是键值对映射。 2)若果Map继承了Collection接口,那么所实现的Map接口的类到底是用Map键值对映射数据还是用Collection的一组数据呢?比如平常所用的hashMap、hashTable、treeMap等都是键值对,所以它继承Collection是完全没意义,而且Map如果继承Collection接口的话,违反了面向对象的接口分离原则。 接口分离原则:客户端不应该依赖它不需要的接口。 另一种定义是类间的依赖关系应该建立在最小的接口上。 接口隔离原则将非常庞大、臃肿的接口拆分成为更小的和更具体的接口,这样客户将会只需要知道他们感兴趣的方法。 接口隔离原则的目的是系统解开耦合,从而容易重构、更改和重新部署,让客户端依赖的接口尽可能地小。 3)Map和List、Set不同,Map放的是键值对,List、Set存放的是一个个的对象。说到底是因为数据结构不同,数据结构不同,操作就不一样,所以接口是分开的,还是接口分离原则。 Collection 和 Collections 有什么区别? java.util.Collection是一个集合接口。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。 Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式。其直接继承接口有List与Set。 Collection ├List │├LinkedList │├ArrayList │└Vector │ └Stack └Set java.util.Collections是一个包装类。它包含有各种有关集合操作的静态多态方法。此类不能实例化,就像一个工具类,服务于Java的Collection框架。 堆和栈的概念,它们有什么区别和联系? 在说堆和栈之前,我们先说一下JVM(虚拟机)内存的划分: Java程序在运行时都要开辟空间,任何软件在运行时都要在内存中开辟空间,Java虚拟机运行时也是要开辟空间的。JVM运行时在内存中开辟一片内存区域,启动时在自己的内存区域中进行更细致的划分,因为虚拟机中每一片内存处理的方式都不同,所以要单独进行管理。 JVM内存的划分有五片: 1)寄存器; 2)本地方法区; 3)方法区; 4)栈内存; 5)堆内存。 重点来说一下堆和栈: 栈内存:栈内存首先是一片内存区域,存储的都是局部变量,凡是定义在方法中的都是局部变量(方法外的是全局变量),for循环内部定义的也是局部变量,是先加载函数才能进行局部变量的定义,所以方法先进栈,然后再定义变量,变量有自己的作用域,一旦离开作用域,变量就会被释放。栈内存的更新速度很快,因为局部变量的生命周期都很短。 堆内存:存储的是数组和对象(其实数组就是对象),凡是new建立的都是在堆中,堆中存放的都是实体(对象),实体用于封装数据,而且是封装多个(实体的多个属性),如果一个数据消失,这个实体也没有消失,还可以用,所以堆是不会随时释放的,但是栈不一样,栈里存放的都是单个变量,变量被释放了,那就没有了。堆里的实体虽然不会被释放,但是会被当成垃圾,Java有垃圾回收机制不定时的收取。 比如主函数里的语句 int [] arr=new int [3];在内存中是怎么被定义的: 主函数先进栈,在栈中定义一个变量arr,接下来为arr赋值,但是右边不是一个具体值,是一个实体。实体创建在堆里,在堆里首先通过new关键字开辟一个空间,内存在存储数据的时候都是通过地址来体现的,地址是一块连续的二进制,然后给这个实体分配一个内存地址。数组都是有一个索引,数组这个实体在堆内存中产生之后每一个空间都会进行默认的初始化(这是堆内存的特点,未初始化的数据是不能用的,但在堆里是可以用的,因为初始化过了,但是在栈里没有),不同的类型初始化的值不一样。所以堆和栈里就创建了变量和实体: 那么堆和栈是怎么联系起来的呢? 刚刚说过给堆分配了一个地址,把堆的地址赋给arr,arr就通过地址指向了数组。所以arr想操纵数组时,就通过地址,而不是直接把实体都赋给它。这种我们不再叫他基本数据类型,而叫引用数据类型。称为arr引用了堆内存当中的实体。可以理解为c或“c++”的指针,Java成长自“c++”和“c++”很像,优化了“c++” 如果当int [] arr=null; arr不做任何指向,null的作用就是取消引用数据类型的指向。 当一个实体,没有引用数据类型指向的时候,它在堆内存中不会被释放,而被当做一个垃圾,在不定时的时间内自动回收,因为Java有一个自动回收机制,(而“c++”没有,需要程序员手动回收,如果不回收就越堆越多,直到撑满内存溢出,所以Java在内存管理上优于“c++”)。自动回收机制(程序)自动监测堆里是否有垃圾,如果有,就会自动的做垃圾回收的动作,但是什么时候收不一定。 所以堆与栈的区别很明显: 1)栈内存存储的是局部变量而堆内存存储的是实体; 2)栈内存的更新速度要快于堆内存,因为局部变量的生命周期很短; 3)栈内存存放的变量生命周期一旦结束就会被释放,而堆内存存放的实体会被垃圾回收机制不定时的回收。 Class.forName 和 ClassLoader 有什么区别? 在java中对类进行加载可以使用Class.forName()和ClassLoader。 ClassLoader遵循双亲委派模型,最终调用启动类加载器的类加载器,实现的功能是“通过一个类的全限定名来获取描述此类的二进制字节流”,获取到二进制流后放到JVM中。 Class.forName()方法实际上也是调用的ClassLoader来实现的。 通过分析源码可以得出:最后调用的方法是forName()方法,方法中的第2个参数默认设置为true,该参数表示是否对加载的类进行初始化,设置为true时会对类进行初始化,这就意味着会执行类中的静态代码块以及对静态变量的赋值等操作。 也可以自行调用Class.forName(String name, boolean initialize,ClassLoader loader)方法手动选择在加载类的时候是否要对类进行初始化。 JDK源码中对参数initialize的描述是:if {@code true} the class will be initialized,大概意思是说:当值为true,则加载的类将会被初始化。 为什么要使用设计模式? 1)设计模式是前人根据经验总结出来的,使用设计模式,就相当于是站在了前人的肩膀上。 2)设计模式使程序易读。熟悉设计模式的人应该能够很容易读懂运用设计模式编写的程序。 3)设计模式能使编写的程序具有良好的可扩展性,满足系统设计的开闭原则。比如策略模式,就是将不同的算法封装在子类中,在需要添加新的算法时,只需添加新的子类,实现规定的接口,即可在不改变现有系统源码的情况下加入新的系统行为。 4)设计模式能降低系统中类与类之间的耦合度。比如工厂模式,使依赖类只需知道被依赖类所实现的接口或继承的抽象类,使依赖类与被依赖类之间的耦合度降低。 5)设计模式能提高代码的重用度。比如适配器模式,就能将系统中已经存在的符合新需求的功能代码兼容新的需求提出的接口 。 6)设计模式能为常见的一些问题提供现成的解决方案。 7)设计模式增加了重用代码的方式。比如装饰器模式,在不使用继承的前提下重用系统中已存在的代码。 为什么 String 类型是被 final 修饰的? 1、为了实现字符串池 final修饰符的作用:final可以修饰类,方法和变量,并且被修饰的类或方法,被final修饰的类不能被继承,即它不能拥有自己的子类,被final修饰的方法不能被重写, final修饰的变量,无论是类属性、对象属性、形参还是局部变量,都需要进行初始化操作。 String为什么要被final修饰主要是为了”安全性“和”效率“的原因。 final修饰的String类型,代表了String不可被继承,final修饰的char[]代表了被存储的数据不可更改性。虽然final修饰的不可变,但仅仅是引用地址不可变,并不代表了数组本身不会改变。 为什么保证String不可变呢? 因为只有字符串是不可变,字符串池才有可能实现。字符串池的实现可以在运行时节约很多heap空间,不同的字符串变量都指向池中的同一个字符串。但如果字符串是可变的,那么String interning将不能实现,反之变量改变它的值,那么其它指向这个值的变量值也会随之改变。 如果字符串是可变,会引起很严重的安全问题。如数据库的用户名、密码都是以字符串的形式传入来获得数据库的连接或在socket编程中,主机名和端口都是以字符串的形式传入。因为字符串是不可变的,所以它的值是不可改变的,否则改变字符串指向的对象值,将造成安全漏洞。 2、为了线程安全 因为字符串是不可变的,所以是多线程安全的,同一个字符串实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步。字符串自己便是线程安全的。 3、为了实现String可创建HashCode不可变性 因为字符串是不可变的,所以在它创建的时候HashCode就被缓存了,不需要重新计算。使得字符串很适合作为Map键值对中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。 ​final 关键字的基本用法? 在Java中final关键字可以用来修饰类、方法和变量(包括成员变量和局部变量)。下面从这三个方面来了解一下final关键字的基本用法。 1、修饰类 当用final修饰一个类时,表明这个类不能被继承。也就是说,如果一个类你永远不会让他被继承,就可以用final进行修饰。final类中的成员变量可以根据需要设为final,但是要注意final类中的所有成员方法都会被隐式地指定为final方法。 在使用final修饰类的时候,要注意谨慎选择,除非这个类真的在以后不会用来继承或者出于安全的考虑,尽量不要将类设计为final类。 2、修饰方法 下面这段话摘自《Java编程思想》第四版第143页: “使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升。在最近的Java版本中,不需要使用final方法进行这些优化了。“ 因此,如果只有在想明确禁止该方法在子类中被覆盖的情况下才将方法设置为final的。即父类的final方法是不能被子类所覆盖的,也就是说子类是不能够存在和父类一模一样的方法的。 final修饰的方法表示此方法已经是“最后的、最终的”含义,亦即此方法不能被重写(可以重载多个final修饰的方法)。此处需要注意的一点是:因为重写的前提是子类可以从父类中继承此方法,如果父类中final修饰的方法同时访问控制权限为private,将会导致子类中不能直接继承到此方法,因此,此时可以在子类中定义相同的方法名和参数,此时不再产生重写与final的矛盾,而是在子类中重新定义了新的方法。(注:类的private方法会隐式地被指定为final方法。) 3、修饰变量 final成员变量表示常量,只能被赋值一次,赋值后值不再改变。 当final修饰一个基本数据类型时,表示该基本数据类型的值一旦在初始化后便不能发生变化;如果final修饰一个引用类型时,则在对其初始化之后便不能再让其指向其他对象了,但该引用所指向的对象的内容是可以发生变化的。本质上是一回事,因为引用的值是一个地址,final要求值,即地址的值不发生变化。 final修饰一个成员变量(属性),必须要显示初始化。这里有两种初始化方式,一种是在变量声明的时候初始化;第二种方法是在声明变量的时候不赋初值,但是要在这个变量所在的类的所有的构造函数中对这个变量赋初值。 当函数的参数类型声明为final时,说明该参数是只读型的。即你可以读取使用该参数,但是无法改变该参数的值。 如何理解 final 关键字? 1)类的final变量和普通变量有什么区别? 当用final作用于类的成员变量时,成员变量(注意是类的成员变量,局部变量只需要保证在使用之前被初始化赋值即可)必须在定义时或者构造器中进行初始化赋值,而且final变量一旦被初始化赋值之后,就不能再被赋值了。 2)被final修饰的引用变量指向的对象内容可变吗? 引用变量被final修饰之后,虽然不能再指向其他对象,但是它指向的对象的内容是可变的 3)final参数的问题 在实际应用中,我们除了可以用final修饰成员变量、成员方法、类,还可以修饰参数、若某个参数被final修饰了,则代表了该参数是不可改变的。如果在方法中我们修改了该参数,则编译器会提示你: The final local variable i cannot be assigned. It must be blank and not using a compound assignment。 java采用的是值传递,对于引用变量,传递的是引用的值,也就是说让实参和形参同时指向了同一个对象,因此让形参重新指向另一个对象对实参并没有任何影响。 ArrayList 和 LinkedList 有什么区别? 1)ArrayList是Array动态数组的数据结构,LinkedList是Link链表的数据结构,此外,它们两个都是对List接口的实现。前者是数组队列,相当于动态数组;后者为双向链表结构,也可当作堆栈、队列、双端队列。 2)当随机访问List时(get和set操作),ArrayList比LinkedList的效率更高,因为LinkedList是线性的数据存储方式,所以需要移动指针从前往后依次查找。 3)当对数据进行增加和删除的操作时(add和remove操作),LinkedList比ArrayList的效率更高,因为ArrayList是数组,所以在其中进行增删操作时,会对操作点之后所有数据的下标索引造成影响,需要进行数据的移动。 4)从利用效率来看,ArrayList自由性较低,因为它需要手动设置固定大小的容量,但是它的使用比较方便,只需要创建,然后添加数据,通过调用下标进行使用;而LinkedList自由性较高,能够动态的随数据量的变化而变化,但是它不便于使用。 5)ArrayList主要控件开销在于需要在List列表预留一定空间;而LinkList主要控件开销在于需要存储结点信息以及结点指针信息。 HashMap 和 HashTable 有什么区别? Hashtable是线程安全,而HashMap则非线程安全。 Hashtable所有实现方法添加了synchronized关键字来确保线程同步,因此相对而言HashMap性能会高一些,平时使用时若无特殊需求建议使用HashMap,在多线程环境下若使用HashMap需要使用Collections.synchronizedMap()方法来获取一个线程安全的集合。 HashMap允许使用null作为key,不过建议还是尽量避免使用null作为key。HashMap以null作为key时,总是存储在table数组的第一个节点上。而Hashtable则不允许null作为key。 HashMap继承了AbstractMap,HashTable继承Dictionary抽象类,两者均实现Map接口。 HashMap的初始容量为16,Hashtable初始容量为11,两者的填充因子默认都是0.75。 HashMap扩容时是当前容量翻倍即:capacity*2,Hashtable扩容时是容量翻倍+1即:capacity*2+1。 HashMap和Hashtable的底层实现都是数组+链表结构实现。 线程的生命周期包括哪几个阶段? 当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)五种状态。当线程启动以后,它不能一直占用着CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换。 线程的生命周期包含5个阶段,包括:新建、就绪、运行、阻塞、死亡。 新建(new Thread) 当创建Thread类的一个实例(对象)时,此线程进入新建状态(未被启动)。 就绪(runnable) 线程已经被启动,正在等待被分配给CPU时间片,也就是说此时线程正在就绪队列中排队等候得到CPU资源 运行(running) 线程获得CPU资源正在执行任务(run()方法),此时除非此线程自动放弃CPU资源或者有优先级更高的线程进入,线程将一直运行到结束。 堵塞(blocked) 由于某种原因导致正在运行的线程让出CPU并暂停自己的执行,即进入堵塞状态。 正在睡眠:用sleep(long t) 方法可使线程进入睡眠方式。一个睡眠着的线程在指定的时间过去可进入就绪状态。 正在等待:调用wait()方法。(调用motify()方法回到就绪状态) 被另一个线程所阻塞:调用suspend()方法。(调用resume()方法恢复) 死亡(dead) 当线程执行完毕或被其它线程杀死,线程就进入死亡状态,这时线程不可能再进入就绪状态等待执行。 自然终止:正常运行run()方法后终止 异常终止:调用stop()方法让一个线程终止运行 Thread 类中的 start() 和 run() 方法有什么区别? Thread类中通过start()方法来启动一个线程,此时线程处于就绪状态,可以被JVM来调度执行,在调度过程中,JVM通过调用Thread类的run()方法来完成实际的业务逻辑,当run()方法结束后,此线程就会终止,所以通过start()方法可以达到多线程的目的。 如果直接调用线程类的run()方法,会被当做一个普通的函数调用,程序中仍然只有主线程这一个线程,即start()方法呢能够异步的调用run()方法,但是直接调用run()方法确实同步的,无法达到多线程的目的。 notify 和 notifyAll 有什么区别? Java中提供了notify()和notifyAll()两个方法来唤醒在某些条件下等待的线程。 当调用notify()方法时,只有一个等待线程会被唤醒而且它不能保证哪个线程会被唤醒,这取决于线程调度器。 当调用notifyAll()方法时,等待该锁的所有线程都会被唤醒,但是在执行剩余的代码之前,所有被唤醒的线程都将争夺锁。 如果线程调用了对象的wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。 当有线程调用了对象的notifyAll()方法(唤醒所有 wait 线程)或notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。 也就是说,调用了notify后只要一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争优先级高的线程竞争到对象锁的概率大,若某线程没有竞争到该对象锁,它将会留在锁池中,唯有线程再次调用wait()方法,才会重新回到等待池中。 而竞争到对象锁的线程则继续往下执行,直到执行完了synchronized代码块,释放掉该对象锁,此时锁池中的线程会继续竞争该对象锁。 因此,notify()和notifyAll()之间的关键区别在于notify()只会唤醒一个线程,而notifyAll()方法将唤醒所有线程。 什么是乐观锁,什么是悲观锁? 乐观锁 乐观锁的意思是乐观思想,即认为读多写少,遇到并发写的可能性低,每次获取数据时都认为不会被修改,因此不会上锁,但是在更新操作时会判断有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读比较写的操作。 Java中的乐观锁基本上都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,反之失败。 悲观锁 悲观锁的意思是悲观思想,即认为写多,遇到并发写的可能性高,每次获取数据时都认为会被修改,因此每次在读写数据时都会上锁,在读写数据时就会block直到拿到锁。 Java中的悲观锁就是Synchronized、AQS框架下的锁则是先尝试CAS乐观锁去获取锁,获取不到,才会转换为悲观锁,如RetreenLock。 Java 中 volatile 关键字有什么作用? Java语言提供了弱同步机制,即volatile变量,以确保变量的更新通知其他线程。 volatile变量具备变量可见性、禁止重排序两种特性。 volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。 volatile变量的两种特性: 变量可见性 保证该变量对所有线程可见,这里的可见性指的是当一个线程修改了变量的值,那么新的值对于其他线程是可以立即获取的。 禁止重排序 volatile禁止了指令重排。比sychronized更轻量级的同步锁。在访问volatile 变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。 volatile适合场景:一个变量被多个线程共享,线程直接给这个变量赋值。 当对非volatile 变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同CPU cache中。而声明变量是volatile的,JVM 保证了每次读变量都从内存中读,跳过CPU cache这一步。 适用场景 值得说明的是对volatile变量的单次读/写操作可以保证原子性的,如long和double类型变量,但是并不能保证“i++”这种操作的原子性,因为本质上i++是读、写两次操作。在某些场景下可以代替Synchronized。但是,volatile的不能完全取代Synchronized的位置,只有在一些特殊的场景下,才能适用volatile。 总体来说,需要必须同时满足下面两个条件时才能保证并发环境的线程安全: 1)对变量的写操作不依赖于当前值(比如 “i++”),或者说是单纯的变量赋值(boolean flag = true)。 2)该变量没有包含在具有其他变量的不变式中,也就是说,不同的volatile变量之间,不 能互相依赖。只有在状态真正独立于程序内其他内容时才能使用volatile。 Spring 中常用的注解包含哪些? 1)声明bean的注解 @Component 组件,没有明确的角色 @Service 在业务逻辑层使用(service层) @Repository 在数据访问层使用(dao层) @Controller 在展现层使用,控制器的声明(C*上使用) 2)注入bean的注解 @Autowired:由Spring提供 @Inject:由JSR-330提供 @Resource:由JSR-250提供 都可以注解在set方法和属性上,推荐注解在属性上(一目了然,少写代码)。 3)java配置类相关注解 @Configuration 声明当前类为配置类,相当于xml形式的Spring配置(类上使用) @Bean 注解在方法上,声明当前方法的返回值为一个bean,替代xml中的方式(方法上使用) @Configuration 声明当前类为配置类,其中内部组合了@Component注解,表明这个类是一个bean(类上使用) @ComponentScan 用于对Component进行扫描,相当于xml中的(类上使用) @WishlyConfiguration 为@Configuration与@ComponentScan的组合注解,可以替代这两个注解 4)切面(AOP)相关注解 Spring支持AspectJ的注解式切面编程。 @Aspect 声明一个切面(类上使用) 使用@After、@Before、@Around定义建言(advice),可直接将拦截规则(切点)作为参数。 @After 在方法执行之后执行(方法上使用) @Before 在方法执行之前执行(方法上使用) @Around 在方法执行之前与之后执行(方法上使用) @PointCut 声明切点 在java配置类中使用@EnableAspectJAutoProxy注解开启Spring对AspectJ代理的支持(类上使用) 5)@Bean的属性支持 @Scope 设置Spring容器如何新建Bean实例(方法上使用,得有@Bean) 其设置类型包括: Singleton (单例,一个Spring容器中只有一个bean实例,默认模式), Protetype (每次调用新建一个bean), Request (web项目中,给每个http request新建一个bean), Session (web项目中,给每个http session新建一个bean), GlobalSession(给每一个 global http session新建一个Bean实例) @StepScope 在Spring Batch中还有涉及 @PostConstruct 由JSR-250提供,在构造函数执行完之后执行,等价于xml配置文件中bean的initMethod @PreDestory 由JSR-250提供,在Bean销毁之前执行,等价于xml配置文件中bean的destroyMethod 6)@Value注解 @Value 为属性注入值(属性上使用) 7)环境切换 @Profile 通过设定Environment的ActiveProfiles来设定当前context需要使用的配置环境。(类或方法上使用) @Conditional Spring4中可以使用此注解定义条件话的bean,通过实现Condition接口,并重写matches方法,从而决定该bean是否被实例化。(方法上使用) 8)异步相关 @EnableAsync 配置类中,通过此注解开启对异步任务的支持,叙事性AsyncConfigurer接口(类上使用) @Async 在实际执行的bean方法使用该注解来申明其是一个异步任务(方法上或类上所有的方法都将异步,需要@EnableAsync开启异步任务) 9)定时任务相关 @EnableScheduling 在配置类上使用,开启计划任务的支持(类上使用) @Scheduled 来申明这是一个任务,包括cron,fixDelay,fixRate等类型(方法上,需先开启计划任务的支持) 10)@Enable*注解说明 注解主要用来开启对xxx的支持。 @EnableAspectJAutoProxy 开启对AspectJ自动代理的支持 @EnableAsync 开启异步方法的支持 @EnableScheduling 开启计划任务的支持 @EnableWebMvc 开启Web MVC的配置支持 @EnableConfigurationProperties 开启对@ConfigurationProperties注解配置Bean的支持 @EnableJpaRepositories 开启对SpringData JPA Repository的支持 @EnableTransactionManagement 开启注解式事务的支持 @EnableTransactionManagement 开启注解式事务的支持 @EnableCaching 开启注解式的缓存支持 11)测试相关注解 @RunWith 运行器,Spring中通常用于对JUnit的支持 @ContextConfiguration 用来加载配置ApplicationContext,其中classes属性用来加载配置类 Spring MVC 中常用的注解包含哪些? @EnableWebMvc 在配置类中开启Web MVC的配置支持,如一些ViewResolver或者MessageConverter等,若无此句,重写WebMvcConfigurerAdapter方法(用于对SpringMVC的配置)。 @Controller 声明该类为SpringMVC中的Controller @RequestMapping 用于映射Web请求,包括访问路径和参数(类或方法上) @ResponseBody 支持将返回值放在response内,而不是一个页面,通常用户返回json数据(返回值旁或方法上) @RequestBody 允许request的参数在request体中,而不是在直接连接在地址后面。(放在参数前) @PathVariable 用于接收路径参数,比如@RequestMapping(“/hello/{name}”)申明的路径,将注解放在参数中前,即可获取该值,通常作为Restful的接口实现方法。 @RestController 该注解为一个组合注解,相当于@Controller和@ResponseBody的组合,注解在类上,意味着,该Controller的所有方法都默认加上了@ResponseBody。 @ControllerAdvice 通过该注解,我们可以将对于控制器的全局配置放置在同一个位置,注解了@Controller的类的方法可使用@ExceptionHandler、@InitBinder、@ModelAttribute注解到方法上, 这对所有注解了 @RequestMapping的控制器内的方法有效。 @ExceptionHandler 用于全局处理控制器里的异常 @InitBinder 用来设置WebDataBinder,WebDataBinder用来自动绑定前台请求参数到Model中。 @ModelAttribute 本来的作用是绑定键值对到Model里,在@ControllerAdvice中是让全局的@RequestMapping都能获得在此处设置的键值对。 为什么说 MyBatis 是半自动 ORM 映射? ORM是Object和Relation之间的映射,包括Object->Relation和Relation->Object两方面。Hibernate是个完整的ORM框架,而MyBatis完成的是Relation->Object,也就是其所说的Data Mapper Framework。 JPA是ORM映射标准,主流的ORM映射都实现了这个标准。MyBatis没有实现JPA,它和ORM框架的设计思路不完全一样。MyBatis是拥抱SQL,而ORM则更靠近面向对象,不建议写SQL,实在要写需用框架自带的类SQL代替。MyBatis是SQL映射而不是ORMORM映射,当然ORM和MyBatis都是持久层框架。 最典型的ORM映射是Hibernate,它是全自动ORM映射,而MyBatis是半自动的ORM映射。Hibernate完全可以通过对象关系模型实现对数据库的操作,拥有完整的JavaBean对象与数据库的映射结构来自动生成SQL。而MyBatis仅有基本的字段映射,对象数据以及对象实际关系仍然需要通过手写SQL来实现和管理。 Hibernate数据库移植性远大于MyBatis。Hibernate通过它强大的映射结构和HQL语言,大大降低了对象与数据库(oracle、mySQL等)的耦合性,而MyBatis由于需要手写SQL,因此与数据库的耦合性直接取决于程序员写SQL的方法,如果SQL不具通用性而用了很多某数据库特性的SQL语句的话,移植性也会随之降低很多,成本很高。 main 方法中 args 参数是什么含义? java中args即为arguments的缩写,是指字符串变量名,属于引用变量,属于命名,可以自定义名称也可以采用默认值,一般习惯性照写。 String[] args是main函数的形式参数,可以用来获取命令行用户输入进去的参数。 1)字符串变量名(args)属于引用变量,属于命名,可以自定义名称。 2)可以理解成用于存放字符串数组,若去掉无法知晓"args"声明的变量是什么类型。 3)假设public static void main方法,代表当启动程序时会启动这部分; 4)String[] args是main函数的形式参数,可以用来获取命令行用户输入进去的参数。 5)java本身不存在不带String args[]的main函数,java程序中去掉String args[]会出现错误。 什么是高内聚、低耦合? 内聚关注模块内部的元素结合程度,耦合关注模块之间的依赖程度。 1)内聚性 又称块内联系。指模块的功能强度的度量,即一个模块内部各个元素彼此结合的紧密程度的度量。若一个模块内各元素(语名之间、程序段之间)联系的越紧密,则它的内聚性就越高。 所谓高内聚是指一个软件模块是由相关性很强的代码组成,只负责一项任务,也就是常说的单一责任原则。 2)耦合性 也称块间联系。指软件系统结构中各模块间相互联系紧密程度的一种度量。模块之间联系越紧密,其耦合性就越强,模块的独立性则越差。模块间耦合高低取决于模块间接口的复杂性、调用的方式及传递的信息。 对于低耦合,粗浅的理解是:一个完整的系统,模块与模块之间,尽可能的使其独立存在。也就是说,让每个模块,尽可能的独立完成某个特定的子功能。模块与模块之间的接口,尽量的少而简单。如果某两个模块间的关系比较复杂的话,最好首先考虑进一步的模块划分。这样有利于修改和组合。 Spring Boot 框架的优缺点? Spring Boot优点 1)创建独立的Spring应用程序 Spring Boot以jar包的形式独立运行,使用java -jar xx.jar命令运行项目或在项目的主程序中运行main方法。 2)Spring Boot内嵌入Tomcat,Jetty或者Undertow,无序部署WAR包文件 Spring项目部署时需要在服务器上部署tomcat,然后把项目打成war包放到tomcat中webapps目录。 Spring Boot项目不需要单独下载Tomcat等传统服务器,内嵌容器,使得可以执行运行项目的主程序main函数,让项目快速运行,另外,也降低对运行环境的基本要求,环境变量中有JDK即可。 3)Spring Boot允许通过maven工具根据需要获取starter Spring Boot提供了一系列的starter pom用来简化我们的Maven依赖,通过这些starter项目就能以Java Application的形式运行Spring Boot项目,而无需其他服务器配置。 starter pom: https://docs.spring.io/spring-boot/docs/1.4.1.RELEASE/reference/htmlsingle/#using-boot-starter 4)Spring Boot尽可能自动配置Spring框架 Spring Boot提供Spring框架的最大自动化配置,使用大量自动配置,使得开发者对Spring的配置减少。 Spring Boot更多的是采用Java Config的方式,对Spring进行配置。 5)提供生产就绪型功能,如指标、健康检查和外部配置 Spring Boot提供了基于http、ssh、telnet对运行时的项目进行监控;可以引入spring-boot-start-actuator依赖,直接使用REST方式来获取进程的运行期性能参数,从而达到监控的目的,比较方便。 但是Spring Boot只是微框架,没有提供相应的服务发现与注册的配套功能、监控集成方案以及安全管理方案,因此在微服务架构中,还需要Spring Cloud来配合一起使用,可关注微信公众号“Java精选”,后续篇幅会针对Spring Cloud面试题补充说明。 5)绝对没有代码生成,对XML没有要求配置 Spring Boot优点 1)依赖包太多,一个spring Boot项目就需要很多Maven引入所需的jar包 2)缺少服务的注册和发现等解决方案 3)缺少监控集成、安全管理方案 Spring Boot 核心注解都有哪些? 1)@SpringBootApplication* 用于Spring主类上最最最核心的注解,自动化配置文件,表示这是一个SpringBoot项目,用于开启SpringBoot的各项能力。 相当于@SpringBootConfigryation、@EnableAutoConfiguration、@ComponentScan三个注解的组合。 2)@EnableAutoConfiguration 允许SpringBoot自动配置注解,开启这个注解之后,SpringBoot就能根据当前类路径下的包或者类来配置Spring Bean。 如当前路径下有MyBatis这个Jar包,MyBatisAutoConfiguration 注解就能根据相关参数来配置Mybatis的各个Spring Bean。 3)@Configuration Spring 3.0添加的一个注解,用来代替applicationContext.xml配置文件,所有这个配置文件里面能做到的事情都可以通过这个注解所在的类来进行注册。 4)@SpringBootConfiguration @Configuration注解的变体,只是用来修饰Spring Boot的配置而已。 5)@ComponentScan Spring 3.1添加的一个注解,用来代替配置文件中的component-scan配置,开启组件扫描,自动扫描包路径下的@Component注解进行注册bean实例放到context(容器)中。 6)@Conditional Spring 4.0添加的一个注解,用来标识一个Spring Bean或者Configuration配置文件,当满足指定条件才开启配置 7)@ConditionalOnBean 组合@Conditional注解,当容器中有指定Bean才开启配置。 8)@ConditionalOnMissingBean 组合@Conditional注解,当容器中没有值当Bean才可开启配置。 9)@ConditionalOnClass 组合@Conditional注解,当容器中有指定Class才可开启配置。 10)@ConditionalOnMissingClass 组合@Conditional注解,当容器中没有指定Class才可开启配置。 11)@ConditionOnWebApplication 组合@Conditional注解,当前项目类型是WEB项目才可开启配置。 项目有以下三种类型: ① ANY:任意一个Web项目 ② SERVLET: Servlet的Web项目 ③ REACTIVE :基于reactive-base的Web项目 12) @ConditionOnNotWebApplication 组合@Conditional注解,当前项目类型不是WEB项目才可开启配置。 13)@ConditionalOnProperty 组合@Conditional注解,当指定的属性有指定的值时才可开启配置。 14)@ConditionalOnExpression 组合@Conditional注解,当SpEl表达式为true时才可开启配置。 15)@ConditionOnJava 组合@Conditional注解,当运行的Java JVM在指定的版本范围时才开启配置。 16)@ConditionalResource 组合@Conditional注解,当类路径下有指定的资源才开启配置。 17)@ConditionOnJndi 组合@Conditional注解,当指定的JNDI存在时才开启配置。 18)@ConditionalOnCloudPlatform 组合@Conditional注解,当指定的云平台激活时才可开启配置。 19)@ConditiomalOnSingleCandidate 组合@Conditional注解,当制定的Class在容器中只有一个Bean,或者同时有多个但为首选时才开启配置。 20)@ConfigurationProperties 用来加载额外的配置(如.properties文件),可用在@Configuration注解类或者@Bean注解方法上面。可看一看Spring Boot读取配置文件的几种方式。 21)@EnableConfigurationProperties 一般要配合@ConfigurationProperties注解使用,用来开启@ConfigurationProperties注解配置Bean的支持。 22)@AntoConfigureAfter 用在自动配置类上面,便是该自动配置类需要在另外指定的自动配置类配置完之后。如Mybatis的自动配置类,需要在数据源自动配置类之后。 23)@AutoConfigureBefore 用在自动配置类上面,便是该自动配置类需要在另外指定的自动配置类配置完之前。 24)@Import Spring 3.0添加注解,用来导入一个或者多个@Configuration注解修饰的配置类。 25)@IMportReSource Spring 3.0添加注解,用来导入一个或者多个Spring配置文件,这对Spring Boot兼容老项目非常有用,一位内有些配置文件无法通过java config的形式来配置 Spring Boot 的目录结构是怎样的? 1、代码层的结构 根目录:com.springboot 1)工程启动类(ApplicationServer.java)置于com.springboot.build包下 2)实体类(domain)置于com.springboot.domain 3)数据访问层(Dao)置于com.springboot.repository 4)数据服务层(Service)置于com,springboot.service,数据服务的实现接口(serviceImpl)至于com.springboot.service.impl 5)前端控制器(Controller)置于com.springboot.controller 6)工具类(utils)置于com.springboot.utils 7)常量接口类(constant)置于com.springboot.constant 8)配置信息类(config)置于com.springboot.config 9)数据传输类(vo)置于com.springboot.vo 2、资源文件的结构 根目录:src/main/resources 1)配置文件(.properties/.json等)置于config文件夹下 2)国际化(i18n)置于i18n文件夹下 3)spring.xml置于META-INF/spring文件夹下 4)页面以及js/css/image等置于static文件夹下的各自文件下 Spring Boot 需要独立的容器运行吗? Spring Boot项目可以不需要,内置了Tomcat/Jetty等容器,默认Tomcat。 Spring Boot不需要独立的容器就可以运行,因为在Spring Boot工程发布的jar文件里已经包含了tomcat插件的jar文件。 Spring Boot运行时创建tomcat对象实现web服务功能,另外也可以将Spring Boot编译成war包文件放到tomcat中运行。 Spring Boot 运行方式有哪几种? 1)直接执行main方法运行,通过IDE工具运行Application这个类的main方法 2)使用Maven插件spring-boot-plugin方式启动,在Spring Boot应用的根目录下运行mvn spring-boot:run 3)使用mvn install生成jar后通过java -jar命令运行 Spring Boot 自动配置原理是什么? Spring Boot的启动类中使用了@SpringBootApplication注解,里面的@EnableAutoConfiguration注解是自动配置的核心,注解内部使用@Import(AutoConfigurationImportSelector.class)(class文件用来哪些加载配置类)注解来加载配置类,并不是所有的bean都会被加载,在配置类或bean中使用@Condition来加载满足条件的bean。 @EnableAutoConfiguration给容器导入META-INF/spring.factories中定义的自动配置类,筛选有效的自动配置类。每一个自动配置类结合对应的xxxProperties.java读取配置文件进行自动配置功能 Spring Boot 热部署有几种方式? 1)spring-boot-devtools 通过Springboot提供的开发者工具spring-boot-devtools来实现,在pom.xml引用其依赖。 然后在Settings→Build→Compiler中将Build project automatically勾选上,最后按ctrl+shift+alt+/ 选择registy,将compiler.automake.allow.when.app.running勾选。 2)Spring Loaded Spring官方提供的热部署程序,实现修改类文件的热部署 下载Spring Loaded(项目地址https://github.com/spring-projects/spring-loaded) 添加运行时参数:-javaagent:C:/springloaded-1.2.5.RELEASE.jar –noverify 3)JRebel 收费的一个热部署软件,安装插件使用即可。 MyBatis 中 $ 和 # 传参有什么区别? 1)“#”符号将传入的数据当成一个字符串并将传入的数据加上双引号。 如:order by #{userId},如果传入的值是1,那么解析成sql时的值为order by "1",如果传入的值是userId,则解析成的sql为order by "userId"。 2)“$”符号将传入的数据直接显示生成在sql语句中。 如:order by ${userId},如果传入的值是1,那么解析成sql时的值为order by 1, 如果传入的值是userId,则解析成的sql为order by userId。 3)“#”符号能够很大程度防止sql注入,而“$”符号无法防止sql注入。 4)“$”符号方式一般用于传入数据库对象,例如传入表名。 5)一般能用“#”符号的就别用“$”符号 6)MyBatis排序时使用order by动态参数时需要注意使用“$”符号而不是“#”符号。 MyBatis 如何实现分页? 1)相对原始方法,使用limit分页,需要处理分页逻辑: MySQL数据库使用limit,如: select * from table limit 0,10; --返回0-10行 Oracle数据库使用rownum,如: 从表Sys_option(主键为sys_id)中从第10条记录开始检索20条记录,语句如下: SELECT * FROM (SELECT ROWNUM R,t1.* From Sys_option where rownum < 30 ) t2 Where t2.R >= 10 2)拦截StatementHandler,其实质还是在最后生成limit语句。 3)使用PageHelper插件,目前比较常见的方法。 MyBatis 如何获取自动生成的主键id? 数据插入时获得主键值分为两种情况:支持主键自增数据库和不支持主键自增。 1)对于支持自动生成主键的数据库,如Mysql、sqlServer,可以通过Mybatis元素useGeneratedKeys返回当前插入数据主键值到输入类中。 <insert id="insertTest" useGeneratedKeys="true" keyProperty="id" parameterType="com.kq.domain.IdentityTest"> insert into identity_test(name) values(#{name,jdbcType=VARCHAR}) </insert> 当执行此条插入语句以后,实体类IdentityTest中的Id会被当前插入数据的主键自动填充。 2)对于不支持自动生成主键的数据库。Oracle、DB2等,可以用元素selectKey 回当前插入数据主键值到输入类中。(同时生成一个自定义的随机主键) <insert id="insertTest" useGeneratedKeys="true" keyProperty="id" parameterType="com.kq.domain.IdentityTest"> <selectKey keyProperty="id" resultType="String" order="BEFORE"> SELECT REPLACE(UUID(),'-','') </selectKey> insert into identity_test(name) values(#{name,jdbcType=VARCHAR}) </insert> 当执行此条插入语句以后,实体类IdentityTest中的Id也会被当前插入数据的主键自动填充。 TCP 和 UDP 协议有什么区别? 1)基于连接 TCP是面向连接的协议,而UDP是无连接的协议。即TCP面向连接;UDP是无连接的,即发送数据之前不需要建立连接。 2)可靠性和有序性 TCP 提供交付保证(Tcp通过校验和,重传控制,序号标识,滑动窗口、确认应答实现可靠传输),无差错,不丢失,不重复,且按序到达,也保证了消息的有序性。该消息将以从服务器端发出的同样的顺序发送到客户端,尽管这些消息到网络的另一端时可能是无序的。TCP协议将会为你排好序。 UDP不提供任何有序性或序列性的保证。UDP尽最大努力交付,数据包将以任何可能的顺序到达。 TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道。 3)实时性 UDP具有较好的实时性,工作效率比TCP高,适用于对高速传输和实时性有较高的通信或广播通信。 4)协议首部大小 TCP首部开销20字节;UDP的首部开销小,只有8个字节。 5)运行速度 TCP速度比较慢,而UDP速度比较快,因为TCP必须创建连接,以保证消息的可靠交付和有序性,毕竟TCP协议比UDP复杂。 6)拥塞机制 UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低,对实时应用很有用,如IP电话,实时视频会议等。 7)流模式(TCP)与数据报模式(UDP) TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流;UDP是面向报文的 。 8)资源占用 TCP对系统资源要求较多,UDP对系统资源要求较少。 TCP被认为是重量级的协议,而与之相比,UDP协议则是一个轻量级的协议。因为UDP传输的信息中不承担任何间接创造连接,保证交货或秩序的的信息。这也反映在用于承载元数据的头的大小。 9)应用 每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信 。基于UDP不需要建立连接,所以且适合多播的环境,UDP是大量使用在游戏和娱乐场所。 Integer 类型值是 0 ,为什么 != '' 无法执行? 开发微信小程序“Java精选面试题”后台管理系统时,遇到根据状态判断是或否发布。 MySQL数据库中设计数据库表,其中某字段status使用tinyint数据类型,当修改状态的时候,赋值status属性的值为0,用于改变状态记录试题库中发布情况。 但是通过Debug模式查看Controller控制层明显已经获取到status等于0,但是在执行到MyBatis中xml文件SQL语句时,总是无法赋值成功,xml配置如下: <update id="updateWarehouse" parameterType="Warehouse"> update t_warehouse <set> <if test="title != null and title != ''">title = #{title},</if> <if test="code != null and code != ''">code = #{code},</if> <if test="content != null and content != ''">content = #{content},</if> <if test="status != null and status != ''">status = #{status},</if> <if test="parentId != null and parentId != ''">parentId = #{parentId}</if> </set> where id = #{id} </update> 分析: 通过分析表面上没有任何传参问题,通过上网查询MyBatis相关资料,终于弄明白什么原因。 此行代码中 <if test="status != null and status != ''">status = #{status},</if> and status != '',MyBatis中传参status的值为0时,因为数据类型为Integer类型,判断为false值。 MyBatis认定 0 = ''的,因此判断status != ''为false,这导致修改试题信息时状态值无法改变为0。 正确写法: <update id="updateWarehouse" parameterType="Warehouse"> update t_warehouse <set> <if test="title != null and title != ''">title = #{title},</if> <if test="code != null and code != ''">code = #{code},</if> <if test="content != null and content != ''">content = #{content},</if> <if test="status != null">status = #{status},</if> <if test="parentId != null and parentId != ''">parentId = #{parentId}</if> </set> where id = #{id} </update> MySQL 的索引有哪些设计原则? 选择唯一性索引 唯一性索引的值是唯一的,可以更快速的通过该索引来确定某条记录 作为查询条件的字段建立索引 某个字段用来做查询条件,那么该字段的查询速度会影响整个表的查询速度。因此,为该字段建立索引,可以提高整个表的查询速度 限制索引的数量 索引的数量并不是越多越好,每个索引都需要占用磁盘空间,索引越多,需要的磁盘空间就越大。 修改表时,索引过多会使得更新表速度变慢 尽量使用字段数据量少的索引 若索引的字段数据量过长,会导致查询的速度变慢 如:对一个char(200)类型的字段进行全文检索需要的时间肯定比对char(10)类型的字段需要的时间更多 排序、分组和联合字段建立索引 使用order by、group by、distinct和union等操作的字段进行排序操作会浪费很多时间。若为其建立索引,可以有效的避免排序操作 尽量使用前缀索引 索引字段的值很长,最好使用值的前缀来索引 如:text和blog类型的字段,进行全文检索会浪费时间。若只检索字段的部分若干个字符,可以提高检索速度 删除不使用或者很少使用的索引 表中数据被批量更新或数据的使用方式被改变后,原有的一些索引可能不再需要。应当定期清理这些索引 小表不创建索引(超过200w数据的表,创建索引) 包含很多列且不需要搜索非空值时可以考虑不建索引 被用来过滤记录的字段创建索引 primary key 字段,系统自动创建主键的索引 unique key 字段,系统自动创建对应的索引 foreign key 约束所定义的作为外键的字段 在查询中用来连接表的字段 用作为排序(order by的字段)的字段 创建索引必须考虑数据的操作方式,原则是内容变动少,经常被用于查询的字段创建索引,对于经常性变动的表而言,则需要谨慎创建必要的索引 为什么要使用自增 ID 作为主键? 1、若定义主键(PRIMARY KEY),InnoDB会选择主键作为聚集索引,反之未定义主键,则InnoDB会选择第一个不包含NULL值的唯一索引作为主键索引。 没有唯一索引,则InnoDB会选择内置6字节长的ROWID作为隐含的聚集索引(ROWID随着行记录的写入而主键递增,这个ROWID不像ORACLE的ROWID那样可引用,是隐含的)。 2、数据记录本身被存于主索引(一颗B+Tree)的叶子节点上,这就要求同一个叶子节点内(大小为一个内存页或磁盘页)的各条数据记录按主键顺序存放。 每当有一条新的记录插入时,MySQL会根据其主键将其插入适当的节点和位置,若页面达到装载因子(InnoDB默认为15/16),则开辟一个新的页(节点)。 3、若表使用自增主键,则每次插入新的记录就会顺序添加到当前索引节点的后续位置,当写满一页就会自动开辟一个新的页。 4、若使用非自增主键,因为每次插入主键的值都近似于随机,所以每次新纪录都要被插到现有索引页得中间某个位置,此时MySQL将为新记录插到合适位置而移动数据,甚至可能被回写到磁盘上而从缓存中清除掉,此时又要从磁盘上读回来,这将增大了开销。同时频繁的移动、分页操作造成了大量的碎片,得到了不够紧凑的索引结构,后续需通过OPTIMIZE TABLE来重建表并优化填充页面。 Linux 如何切换用户? Linux系统切换用户的命令是su su的含义是(switch user)切换用户的缩写。 通过su命令,可以从普通用户切换到root用户,也可以从root用户切换到普通用户。 <font color="#dd0000">注:从普通用户切换到root用户需要密码,从root用户切换到普通用户不需要密码。</font> 使用SecureCRT工具连接终端,由普通用户切换到root用户。 在终端输入su命令然后回车,要求输入密码(linux终端输入的密码不显示)输入密码后回车进入root用户。 在终端输入su root然后回车,也可以进入root用户或su - root回车,也可以切换root用户。针对su root与su - root区别之后的篇幅会阐述,欢迎关注微信公众号“Java精选”,送免费资料。 su root 和 su - root 有什么区别? su后面不加用户是默认切到root,su是不改变当前变量;su -是改变为切换到用户的变量。 简单来说就是su只能获得root的执行权限,不能获得环境变量,而su -是切换到root并获得root的环境变量及执行权限。 语法: $ su [user_name] su命令可以用来更改你的用户ID和组ID。su是switch user或set user id的一个缩写。 此命令是用于开启一个子进程,成为新的用户ID和赋予存取与这个用户ID关联所有文件的存取权限。 出于安全的考虑,在实际转换身份时,会被要求输入这个用户帐号的密码。 如果没有参数,su命令将转换为root(系统管理员)。root帐号有时也被称为超级用户,因为这个用户可以存取系统中的任何文件。 当必须要提供root密码,想要回到原先的用户身份,可以不使用su命令,只需使用exit命令退出使用su命令而生成的新的对话进程。 $ su – username 当使用命令su username时,对话特征和原始的登录身份一样。 如果需要对话进程拥有转换后的用户ID一致的特征,使用短斜杠命令: su – username。 Linux 怎么切换目录? 1)使用pwd命令查看一下当前所在的目录 2)切换文件目录使用的cd命令。 切换到根目录命令如下: cd / 3)根目录下使用ls命令查看该目录下有哪些文件,使用用绝对路径的方式进入所需目录,比如进入usr目录,命令如下: cd /usr 4)若进入文件下一层级目录,可以使用相对路径的方式切换目录。比如目录结构如下: usr/local/other 在当前usr目录下使用命令 cd ./local 进入usr目录下的local目录。此命令和使用绝对路径的方式区别在于,前面多了个 ".",这个"."代表的是当前目录。 5)若要回到上一级目录,则可以使用命令 cd ../ Dubbo 支持哪些协议,推荐用哪种? dubbo协议(推荐使用) 单一TCP长连接和NIO异步通讯,适合大并发小数据量的服务调用,以及服务消费者远大于提供者的情况。 缺点是Hessian二进制序列化,不适合传送大数据包的服务 rmi协议 采用JDK标准的rmi协议实现,传输参数和返回参数对象需要实现Serializable接口。 使用java标准序列化机制,使用阻塞式短连接,传输数据包不限,消费者和提供者个数相当。 多个短连接,TCP协议传输,同步传输,适用常规的远程服务调用和rmi互操作。 缺点是在依赖低版本的Common-Collections包,java反序列化存在安全漏洞,需升级commons-collections3 到3.2.2版本或commons-collections4到4.1版本。 webservice协议 基于WebService的远程调用协议(Apache CXF的frontend-simple和transports-http)实现,提供和原生WebService的互操作。 多个短连接,基于HTTP传输,同步传输,适用系统集成和跨语言调用。 http协议 基于Http表单提交的远程调用协议,使用Spring的HttpInvoke实现,对传输数据包不限,传入参数大小混合,提供者个数多于消费者。 缺点是不支持传文件,只适用于同时给应用程序和浏览器JS调用。 hessian协议 集成Hessian服务,基于底层Http通讯,采用Servlet暴露服务,Dubbo内嵌Jetty作为服务器实现,可与Hession服务互操作。 通讯效率高于WebService和Java自带的序列化。 适用于传输大数据包(可传文件),提供者比消费者个数多,提供者压力较大。 缺点是参数及返回值需实现Serializable接口,自定义实现List、Map、Number、Date、Calendar等接口 thrift协议 协议:对thrift原生协议的扩展添加了额外的头信息,使用较少,不支持传null值。 memcache协议 基于memcached实现的RPC协议。 redis协议 基于redis实现的RPC协议。 Dubbo 默认使用什么注册中心,还有别的选择吗? 推荐使用Zookeeper作为注册中心,还有Redis、Multicast、Simple注册中心,但不推荐。 为什么 Redis 需把数据放到内存中? 若不将数据读到内存中,磁盘I/O速度会严重影响Redis的性能。 Redis具有快速和数据持久化的特征。 Redis为了提高读写时的速度将数据读到内存中,并通过异步的方式将数据写入磁盘。 注:若设置最大使用内存,则数据已有记录数达到内存限值后不能继续插入新值。 为什么内存读取比磁盘读取数据速度快? 1)内存是电器元件,利用高低电平存储数据,而磁盘是机械元件,电气原件速度超快,而磁盘由于在每个磁盘块切换时磁头会消耗很多时间,说白了就是IO时间长,两者性能没发比较。 2)磁盘的数据进行操作时也是读取到内存中交由CPU进行处理操作,因此直接放在内存中的数据,读取速度肯定快很多。 Zookeeper 怎么保证主从节点的状态同步? Zookeeper的核心是原子广播机制,这个机制保证了各个server之间的同步。实现这个机制的协议叫做Zab协议。 Zab协议有两种模式,分别是恢复模式和广播模式。 恢复模式 当服务启动或者在领导者崩溃后,Zab就进入了恢复模式,当领导者被选举出来,且大多数server完成了和leader的状态同步以后,恢复模式就结束了。 状态同步保证了leader和server具有相同的系统状态。 广播模式 一旦leader已经和多数的follower进行了状态同步后,它就可以开始广播消息了,即进入广播状态。 此时当一个server加入ZooKeeper服务中,它会在恢复模式下启动,发现leader,并和leader进行状态同步。 待到同步结束,它也参与消息广播。 ZooKeeper服务一直维持在Broadcast状态,直到leader崩溃了或者leader失去了大部分的followers支持。 Dubbo 停止更新了吗? Dubbo是阿里巴巴内部使用的分布式业务框架,于2012年由阿里巴巴开源。 由于Dubbo在阿里巴巴内部经过广泛的业务验证,在很短时间内,Dubbo就被许多互联网公司所采用,并产生了许多衍生版本,如网易,京东,新浪,当当等等。 由于阿里巴巴内部策略的调整变化,在2014年10月Dubbo停止维护。随后部分互联网公司公开了自行维护的Dubbo版本,比较著名的如当当DubboX,新浪Motan等。 在2017年9月,阿里宣布重启Dubbo项目,并决策在未来对开源进行长期持续的投入。随后Dubbo开始了密集的更新,并将搁置三年以来大量分支上的特性及缺陷快速修正整合。 在2018年2月,阿里巴巴将Dubbo捐献给Apache基金会,Dubbo成为Apache孵化器项目。 为什么选用 Maven 进行构建? 1)Maven是一个优秀的项目构建工具。 使用Maven可以比较方便的对项目进行分模块构建,这样在开发或测试打包部署时,会大大的提高效率。 2)Maven可以进行依赖的管理。 使用Maven 可以将不同系统的依赖进行统一管理,并且可以进行依赖之间的传递和继承。 Maven可以解决jar包的依赖问题,根据JAR包的坐标去自动依赖/下载相关jar,通过仓库统一管理jar包。 多个项目JAR包冗余,使用Maven解决一致性问题。 4)屏蔽开发工具之间的差异,例如:IDE,Eclipse,maven项目可以无损导入其他编辑器。 Maven 规约是什么? src/main/java 存放项目的类文件(后缀.java文件,开发源代码) src/main/resources 存放项目配置文件,若没有配置文件该目录可无,如Spring、Hibernate、MyBatis等框架配置文件 src/main/webapp 存放web项目资源文件(web项目需要) src/test/java 存放所有测试类文件(后缀.java文件,测试源代码) src/test/resources 测试配置文件,若没有配置文件该目录可无 target 文件编译过程中生成的后缀.class文件、jar、war等 pom.xml maven项目核心配置文件,管理项目构建和依赖的Jar包 Maven负责项目的自动化构建,以编译为例,Maven若果自动进行编译,需要知道Java的源文件保存位置,通过这些规约,不用开发者手动指定位置,Maven就可以清晰的知道相关文件所在位置,从而完成自动编译。 遵循**“约定>>>配置>>>编码”**。即能进行配置的不要去编码指定,能事先约定规则的不要去进行配置。这样既减轻了工作量,也能防止编译出错。 Maven 常用命令有哪些? 1)mvn clean 清理输出目录默认target/ 2)mvn clean compline 编译项目主代码,默认编译至target/classes目录下 3)mvn clean install maven安装,将生成的JAR包文件复制到本地maven仓库中,其他项目可以直接使用这个JAR包 4)mvn clean test maven测试,但实际执行的命令有: clean:clean resource:resources compiler:compile resources:testResources compiler:testCompile maven执行test前,先自动执行项目主资源处理,主代码编译,测试资源处理,测试代码编译等工作 测试代码编译通过之后默认在target/test-calsses目录下生成二进制文件,随后执行surefile:test任务运行测试,并输出测试报告,显示运行多少次测试,失败成功等。 5)mvn celan package maven打包,maven会在打包之前默认执行编译,测试等操作,打包成功之后默认输出在target/目录中 6)mvn help:system 打印出java系统属性和环境变量。 7)echo %MAVEN_HOME%: 查看maven安装路径。 8)mvn deploy 在整合或发布环境下执行,将终版本的包拷贝到远程的repository,使得其他的开发者或者工程可以共享。 9)mvn 检查是否安装了maven。 10)mvn dependency:list 查看当前项目中的已解析依赖 11)mvn dependency:tree 查看当前项目的依赖树 12)mvn dependency:analyse 查看当前项目中使用未声明的依赖和已声明但未使用的依赖 什么是链式存储结构? 链接存储结构的含义是在计算机中用一组任意的存储单元存储线性表的数据元素(这组存储单元可以是连续的,也可以是不连续的),它不要求逻辑上相邻的元素在物理位置上也相邻。 链式存储结构的优点主要是插入和删除简单,前提条件是知道操作位置,时间复杂度是O(1),如果不知道操作位置则要定位元素,时间复杂度为O(n),没有容量的限制,可以使用过程中动态分配的内存空间,不用担心溢出问题,但是它并不能实现随机读取,同时空间利用率不高。 说说几种常见的排序算法和复杂度? 1)快速排序 原理:快速排序采用的是一种分治的思想,通过依次排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。 选定一个合适的值(理想情况下选择中间值最好,但实际中一般使用数组第一个值),称为“枢轴”(pivot)。 基于这个值将数组分为两部分,较小的分在左边,较大的分在右边,如此一轮下来,这个枢轴的位置一定在最终位置上。 对两个子数组分别重复上述过程,直到每个数组只有一个元素,排序完成。 复杂度:O(n) 特点:快速排序是平时最常使用的一种排序算法,因它速度快,效率高的一种排序算法。 2)冒泡排序 原理:冒泡排序采用的事两个相邻的值进行比较,将值大的交换到右侧。就是逐一比较交换,进行内外两次循环,外层循环为遍历所有数值,逐个确定每个位置,内层循环为确定位置后遍历所有后续没有确定位置的数字,与该位置的值进行比较,只要比该位置的值小,就位置交换。 复杂度:O(n^2),最佳时间复杂度为O(n) 特点:冒泡排序在实际开发中使用比较少,更适合数据量比较少的场景,因其效率比较低,但逻辑简单,方便记忆。 3)直接插入排序 原理:直接插⼊排序是从第二个数字开始,逐个取出,插入到之前排好序的数组中。 复杂度:O(n^2),最佳时间复杂度为O(n) 4)直接选择排序 原理:直接选择排序是从第一个位置开始遍历位置,找到剩余未排序的数组中最小值,将最小值做交换位置。 复杂度:O(n^2) 特点:类似冒泡排序其逻辑简单,但效率低,适合少量数据排序。 Java 递归遍历目录下的所有文件? import java.io.File; public class ListFiles { public static void listAll(File directory) { if(!(directory.exists() && directory.isDirectory())) { throw new RuntimeException("目录不存在"); } File[] files = directory.listFiles(); for (File file : files) { System.out.println(file.getPath() + file.getName()); if(file.isDirectory()) { listAll(file); } } } public static void main(String[] args) { File directory = new File("E:\\Program Files (x86)"); listAll(directory); } } JSP 获取 ModelAndView 传参数据问题? Idea开发工具自动创建的web.xml约束太低,导致无法正常获取数据,需要把web.xml约束的信息调整一下,参考如下: <?xml version="1.0" encoding="UTF-8"?> <web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"> Linux 运行 SQL 语句文件报错? 原因分析:Linux下MySQL版本不兼容导致的。 解决办法:把文件中所有的utf8mb4_0900_ai_ci替换为utf8_general_ci以及utf8mb4替换为utf8类型。 如何解决 Linux 显示中文乱码问题? 在Linux中通过locale来设置程序运行的不同语言环境,locale由 ANSI C提供支持。locale的命名规则为_.,如zh_CN.GBK,zh代表中文,CN代表大陆地区,GBK表示字符集。 修改 /etc/locale.conf文件的内容 LANG="zh_CN.UTF-8" 执行命令,使修改文件立刻生效 source /etc/locale.conf IDEA 中 Maven 项目无法自动识别 pom.xml? 方式一 File->Settings->Build,Excecution,Deployment->Build Tools->Maven->Ignored Files 查看是否存在maven pom被勾选,去掉勾选即可。 方式二 右键项目pom.xml文件,选择“add as maven project”,自动导入pom所依赖的jar包。 刷新Maven配置 右键单击项目,在弹出菜单中选择Maven->Reimport菜单项。IDEA将通过网络自动下载相关依赖,并存放在Maven的本地仓库中。 或者将Maven的刷新设置为自动,单击File|Setting菜单项,打开Settings选项卡,在左侧的目录树中展开Maven节点,勾选Import Maven projects automatically选择项。 面向过程与面向对象有什么区别? 面向过程 性能相比面向对象高,因其类调用时需要实例化,开销比较大,消耗资源,比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发,性能是最重要的因素。 面向对象 易维护、复用以及扩展,由于面向对象有封装、继承、多态性等特征,可以设计出低耦合、高内聚的系统,使得更加灵活,易于维护。 Java 编程语言有哪些特点? 1)简单易学; 2)面向对象(封装,继承,多态); 3)平台无关性(Java虚拟机实现平台无关性); 4)可靠性; 5)安全性; 6)支持多线程; 7)支持网络编程并方便易用; 8)编译与解释并存。 重载和重写有什么区别? 重载(Overload) 是指让类以统一的方式处理不同类型数据的一种手段,实质表现就是多个具有不同的参数个数或者不同类型的同名函数,存在于同一个类中,返回值类型不同,是一个类中多态性的一种表现。 调用方法时通过传递不同参数个数和参数类型来决定具体使用哪个方法的多态性。 重写(Override) 是指父类与子类之间的多态性,实质就是对父类的函数进行重新定义。 如果子类中定义某方法与其父类有相同的名称和参数则该方法被重写,需注意的是子类函数的访问修饰权限不能低于父类的。 如果子类中的方法与父类中的某一方法具有相同的方法名、返回类型和参数表,则新方法将覆盖原有的方法,如需父类中原有的方法则可使用super关键字。 静态方法和实例方法有什么不同? 静态方法和实例方法的区别主要体现在两个方面: 其一在外部调用静态方法时,可以使用"类名.方法名"的方式,也可以使用"对象名.方法名"的方式而实例方法只能试用后面这种方式。也就是说,调用静态方法可以无需创建对象进行实例化。 其二静态方法在访问本类的成员时,只允许访问静态成员也就是静态成员变量和静态方法,而不允许访问实例成员变量和实例方法,实例方法是没有这个限制的。 == 和 equals 两者有什么区别? 使用==比较 用于对比基本数据类型的变量,是直接比较存储的 “值”是否相等; 用于对比引用类型的变量,是比较的所指向的对象地址。 使用equals比较 equals方法不能用于对比基本数据类型的变量; 如果没对Object中equals方法进行重写,则是比较的引用类型变量所指向的对象地址,反之则比较的是内容。 HashMap 是怎么扩容的? 当HashMap中元素个数超过数组大小*loadFactor时,需进行数组扩容。 loadFactor默认值为0.75,默认情况下,数组大小为16,HashMap中元素个数超过16 * 0.75=12的时,就把数组的大小扩展为2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以能够预选知道HashMap中元素的个数,应该预设数组的大小,可以有效的提高HashMap的性能。 假设有1000个元素new HashMap(1000),理论上来讲new HashMap(1024)更合适,不过上面已经提过即使是1000个元素,HashMap也会自动设置为1024。但是new HashMap(1024),而0.75*1024 <1000, 为了可以0.75 * size >1000,必须new HashMap(2048),避免了resize的问题。 总结: 添加元素时会检查容器当前元素个数。当HashMap的容量值超过临界值(默认16 * 0.75=12)时扩容。HashMap将会重新扩容到下一个2的指数幂(16->32->64)。调用resize方法,定义长度为新长度(32)的数组,然后对原数组数据进行再Hash。注意的是这个过程比较损耗性能。 JDK1.8 和 JDK1.7 中 ArrayList 的初始容量多少? JDK1.7下ArrayList()初始化后的默认长度是10,源码如下: //无参构造方法 public ArrayList() { this(10); } public ArrayList(int initialCapacity) { super(); if (initialCapacity < 0) throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); this.elementData = new Object[initialCapacity]; } 通过上述源代码可以看出,默认的构造方法中直接指定数组长度为10,同时调用重载的构造方法,创建了长度为10的一个数组。 JDK1.8下ArrayList()初始化后的默认长度是0,源码如下: //无参构造方法 public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; 构造方法中静态类型的数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}是个空数组,数组长度为0,因此JDK1.8下的ArrayList()初始化后默认的数组长度为0。 Arrays.asList() 有什么使用限制? 1)Arrays.asList()方法不适用于基本数据类型 byte short int long float double boolean 2)Arrays.asList()方法把数组与列表链接起来,当更新其中之一时,另一个自动更新。 3)Arrays.asList()方法不支持add和remove方法。 Set 为什么是无序的? Set系列集合添加元素无序的根本原因是底层采用哈希表存储元素。 JDK1.8以下版本:哈希表 = 数组 + 链表 + (哈希算法) JDK1.8及以上版本:哈希表 = 数组 + 链表 + 红黑树 + (哈希算法) 当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。 Comparable 和 Comparator有什么区别? Comparable接口出自java.lang包,它有一个compareTo(Object obj)方法用来排序。 Comparator接口出自java.util包,它有一个compare(Object obj1, Object obj2)方 法用来排序。 一般对集合使用自定义排序时,需要重写compareTo()方法或compare()方法。 当需要对某一个集合实现两种排序方式,比如一个用户对象中的姓名和身份证分别采用一种排序方法。 方式一:重写compareTo()方法实现姓名、身份证排序 方式二:使用自定义的Comparator方法实现姓名、身份证排序 方法三:使用两个Comparator来实现姓名、身份证排序 其中方式二代表只能使用两个参数的形式Collections.sort()。 Collections是一个工具类,sort是其中的静态方法,是用来对List类型进行排序的,它有两种参数形式: public static <T extends Comparable<? super T>> void sort(List<T> list) { list.sort(null); } public static <T> void sort(List<T> list, Comparator<? super T> c) { list.sort(c); } HashMap 中如何实现同步? HashMap可以使用如下代码实现: Map map = Collections.synchronizedMap(new HashMap()); 来达到同步的效果。 具体而言,该方法会返回一个同步的Map集合,这个Map封装了底层HashMap的所有方法,使得底层的HashMap可以在多线程的环境中也能够保证安全性。 List、Set、Map 三者有什么区别? List 存储数据允许不唯一集合,可以有多个元素引用相同的对象且是有序的对象。 Set 不允许重复的集合,不存在多个元素引用相同的对象。 Map 使用键值对存储方式。Map维护与Key有关联的值。两个Key可以引用相同的对象,但Key不能重复,比如Key是String类型,但也可以是任何对象。 多线程实现的方式有几种? 1)继承Thread类创建线程 Thread类本质上是实现了Runnable接口的一个实例,代表一个线程的实例。 启动线程的唯一方法是通过Thread类的start()实例方法。 start()方法将启动一个新线程,并执行run()方法。 这种方式实现多线程比较简单,通过自己的类直接继承Thread,并重写run()方法,就可以启动新线程并执行自己定义的run()方法。 2)实现Runnable接口创建线程 如果自己的类已经继承了两一个类,就无法再继承Thread,因此可以实现一个Runnable接口 3)实现Callable接口,通过FutureTask包装器来创建Thread线程 4)使用<code>ExecutorService</code>、<code>Callable</code>、<code>Future</code>实现有返回结果的线程 <code>ExecutorService</code>、<code>Callable</code>、<code>Future</code>三个接口实际上都是属于Executor框架。 在JDK1.5中引入的新特征,不需要为了得到返回值而大费周折。主要需要返回值的任务必须实现Callable接口;不需要返回值的任务必须实现Runnabel接口。 执行Callable任务后,可以获取一个Future对象,在该对象上调用get()方法可以获取到Callable任务返回的Object了。注意的是get()方法是阻塞的,线程没有返回结果时,该方法会一直等待。 什么是线程局部变量? ThreadLocal并非是一个线程本地实现版本,它并不是一个Thread,而是thread local variable(线程局部变量)。也许把它命名为ThreadLocalVar更合适。 线程局部变量(ThreadLocal)功能非常简单,就是为每一个使用该变量的线程都提供了一个变量值副本,是Java中一种较为特殊的线程绑定机制,使得每一个线程都独立地改变所具有的副本,而不会和其他线程的副本冲突。 Java 中常见的阻塞队列有哪些? ArrayBlockingQueue 最典型的有界队列,其内部是用数组存储元素的,利用ReentrantLock实现线程安全,使用Condition来阻塞和唤醒线程。 LinkedBlockingQueue 内部用链表实现BlockingQueue。如果不指定它的初始容量,那么它容量默认就为整型的最大值Integer.MAX_VALUE,由于这个数非常大,通常不可能放入这么多的数据,所以LinkedBlockingQueue 也被称作无界队列,代表它几乎没有界限。 SynchronousQueue 相比较其他,最大的不同之处在于它的容量为0,所以没有地方暂存元素,导致每次存储或获取数据都要先阻塞,直至有数据或有消费者获取数据。 PriorityBlockingQueue 支持优先级的无界阻塞队列,可以通过自定义类实现compareTo()方法来指定元素排序规则或初始化时通过构造器参数Comparator来指定排序规则。需主要的是插入队列的对象必须是可以比较大小的值,否则会抛出ClassCastException异常。 DelayQueue 具有“延迟”的功能。队列中的可以设定任务延迟多久之后执行,比如“30分钟后未付款自动取消订单”等需要执行的场景。 创建线程池的有几种方式? newCachedThreadPool 创建一个可缓存的线程池,如果线程池长度超过处理需求,可灵活回收空闲线程,如果没有可回收线程,则新建线程。 newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。 newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。 newSingleThreadExecutor 创建一个单线程化的线程池,它只会唯一的工作线程来执行任务,保证所有任务按照指定执行。 查看文件内容有哪些命令? vi 文件名 编辑方式查看,可以修改文件 cat 文件名 显示全部文件内容 tail 文件名 仅查看文件尾部信息,可以指定查看行数 head 文件名 仅查看头部,可以指定查看行数 more 文件名 分页显示文件内容 less 文件名 与more相似,可以往前翻页 命令中可以使用哪几种通配符? “?” 可以替代任意单个字符。 “*” 可以替代任意多个字符。 方括号“[charset]” 可以替代charset集中的任何单个字符,比如[a-z],[abABC] 根据文件名搜索文件有哪些命令? find <指定目录> <指定条件> <指定动作>, whereis 加参数与文件名 locate 只加文件名 bash shell 中 hash 命令有什么作用? Linux中hash命令管理着一个内置的哈希表,记录了已执行过命令的完整路径, 用该命令可以打印出所使用过的命令以及执行的次数。 Linux 中进程有哪几种状态? 1)不可中断状态:进程处于睡眠状态,此时的进程不可中断,进程不响应异步信号。 2)暂停状态/跟踪状态:向进程发送一个SIGSTOP信号,它就会因响应信号而进入TASK_STOPPED状态;当进程正在被跟踪时,它处于TASK_TRACED这个特殊的状态。正在被跟踪指的是进程暂停下来,等待跟踪它的进程对它进行操作。 3)就绪状态:在run_queue队列中的状态 4)运行状态:在run_queue队列中的状态 5)可中断睡眠状态:处于这个状态的进程因为等待某事件的发生,比如等待socket连接等,而被挂起 6)zombie状态(僵尸):父进程没有通过wait系列的系统调用,直接将子进程的task_struct也释放掉。 7)退出状态 Integer 和 int 两者有什么区别? Integer是int的包装类,默认值是null;int是基本数据类型,默认值是0; Integer变量必须实例化后才能使用;int变量不需要; Integer实际是对象的引用,指向此new的Integer对象;int是直接存储数据值。 分析总结 1)Integer与new Integer不相等。new出来的对象被存放在堆,而非new的Integer常量则在常量池,两者内存地址不同,因此判断是false。 2)两个值都是非new Integer,如果值在-128,127区间,则是true,反之为false。 这是因为java在编译Integer i2 = 128时,被翻译成: Integer i2 = Integer.valueOf(128); 而valueOf()函数会对-128到127之间的数进行缓存。 3)两个都是new Integer,两者判断为false,内存地址不同。 4)int和Integer对比不管是否new对象,两者判断都是true,因为会把Integer自动拆箱为int再去比。 什么是 Java 内部类? 内部类是指把A类定义在另一个B类的内部。 例如:把类User定义在类Role中,类User就被称为内部类。 class Role { class User { } } 1、内部类的访问规则 ​ 1)可以直接访问外部类的成员,包括私有 ​2)外部类要想访问内部类成员,必须创建对象 2、内部类的分类 ​1)成员内部类 ​2)局部内部类 ​3)静态内部类 ​4)匿名内部类 常用的垃圾收集器有哪些? Serial 收集器(新生代) Serial即串行,以串行的方式执行,是单线程的收集器。它只会使用一个线程进行垃圾收集工作,GC线程工作时,其它所有线程都将停止工作。 作为新生代垃圾收集器的方式:-XX:+UseSerialGC ParNew 收集器(新生代) Serial收集器的多线程版本,需注意的是ParNew在单核环境下性能比Serial差,在多核条件下有优势。 作为新生代垃圾收集器的方式:-XX:+UseParNewGC Parallel Scavenge 收集器(新生代) 多线程的收集器,目标是提高吞吐量(吞吐量 = 运行用户程序的时间 / (运行用户程序的时间 + 垃圾收集的时间))。 作为新生代垃圾收集器的方式:-XX:+UseParallelGC Serial Old 收集器(老年代) Serial收集器的老年代版本。 作为老年代垃圾收集器的方式:-XX:+UseSerialOldGC Parallel Old 收集器(老年代) Parallel Scavenge收集器的老年代版本。 作为老年代垃圾收集器的方式:-XX:+UseParallelOldGC CMS 收集器(老年代) CMS(Concurrent Mark Sweep),收集器几乎占据着JVM老年代收集器的半壁江山,它划时代的意义就在于垃圾回收线程几乎能做到与用户线程同时工作。 作为老年代垃圾收集器的方式:-XX:+UseConcMarkSweepGC G1 收集器(新生代 + 老年代) 面向服务端应用的垃圾收集器,在多CPU和大内存的场景下有很好的性能。HotSpot开发团队赋予它的使命是未来可以替换掉CMS收集器。关注微信公众号Java精选,内涵视频资料、开源项目、源码分析等等。 作为老年代垃圾收集器的方式:-XX:+UseG1GC 生产环境中应用的 JVM 参数有哪些? -server -Xms6000M -Xmx6000M -Xmn500M -XX:PermSize=500M -XX:MaxPermSize=500M -XX:SurvivorRatio=65536 -XX:MaxTenuringThreshold=0 -Xnoclassgc -XX:+DisableExplicitGC -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0 -XX:+CMSClassUnloadingEnabled -XX:-CMSParallelRemarkEnabled -XX:CMSInitiatingOccupancyFraction=90 -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+PrintClassHistogram -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintHeapAtGC -Xloggc:log/gc.log 什么情况下会发生栈内存溢出? 栈溢出(StackOverflowError) 栈是线程私有的,他的生命周期与线程相同,每个方法在执行时会创建一个栈帧,用来存储局部变量表,操作数栈,动态链接,方法出口灯信息。局部变量表又包含基本数据类型,对象引用类型。 若线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。 若虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。 常用的 JVM 调优配置参数有哪些? 假设参数为-Xms20m -Xmx20m -Xss256k XX比X的稳定性更差,并且版本更新不会进行通知和说明。 1、-Xms s为strating,表示堆内存起始大小 2、-Xmx x为max,表示最大的堆内存,一般来说-Xms和-Xmx的设置为相同大小,当heap自动扩容时,会发生内存抖动,影响程序的稳定性 3、-Xmn n为new,表示新生代大小,-Xss规定了每个线程虚拟机栈(堆栈)的大小。 4、-XX:SurvivorRator=8 表示堆内存中新生代、老年代和永久代的比为8:1:1 5、-XX:PretenureSizeThreshold=3145728 表示当创建(new)的对象大于3M的时候直接进入老年代 6、-XX:MaxTenuringThreshold=15 表示当对象的存活的年龄(minor gc一次加1)大于多少时,进入老年代 7、-XX:-DisableExplicirGC 表示是否(+表示是,-表示否)打开GC日志 什么是类加载器? 类加载器是指把类文件加载到虚拟机中,通过一个类的全限定名(包名+类型)来获取描述该类的二进制字节流。 类加载器是Java语言的一项创新,最开始是为了满足Java Applet的需求而设计的。 类加载器目前在层次划分、程序热部署和代码加密等领域被广泛使用。 类加载器分为哪几类? JVM默认提供了系统类加载器(JDK1.8),包括如下: Bootstrap ClassLoader(系统类加载器) Application ClassLoader(应用程序类加载器) Extension ClassLoader(扩展类加载器) Customer ClassLoader(自定义加载器) 可以自定义一个 java.lang.String 吗? 答案是可以的,但是不能被加载使用。 这个主要是因为加载器的委托机制,在类加载器的结构图中,BootStrap是顶层父类,ExtClassLoader是BootStrap类的子类,ExtClassLoader是AppClassLoader的父类。当使用java.lang.String类时,Java虚拟机会将java.lang.String类的字节码加载到内存中。 加载某个类时,优先使用父类加载器加载需要使用的类。加载自定义java.lang.String类,其使用的加载器是AppClassLoader,根据优先使用父类加载器原理,AppClassLoader加载器的父类为ExtClassLoader,这时加载String使用的类加载器是ExtClassLoader,但类加载器ExtClassLoader在jre/lib/ext目录下并不能找到自定义java.lang.String类。 然后使用ExtClassLoader父类的加载器BootStrap,父类加载器BootStrap在JRE/lib目录的rt.jar找到了String.class,将其加载到内存中。 MyBatis 实现批量插入数据的方式有几种? MyBatis 实现批量插入数据的方式有几种? 1、MyBatis foreach标签 foreach主要用在构建in条件,在SQL语句中进行迭代一个集合。 foreach元素的属性主要有item,index,collection,open,separator,close。 item表示集合中每一个元素进行迭代时的别名 index指定一个名字,用于表示在迭代过程中,每次迭代到的位置 open表示该语句以什么开始 separator表示在每次进行迭代之间以什么符号作为分隔符 close表示以什么结束 collection必须指定该属性,在不同情况下,值是不同的,主要体现3种情况: 若传入单参数且参数类型是List时,collection属性值为list 若传入单参数且参数类型是array数组时,collection的属性值为array 若传入参数是多个时,需要封装成Map 具体用法如下: <insert id="insertForeach" parameterType="java.util.List" useGeneratedKeys="false"> insert into t_userinfo (name, age, sex) values <foreach collection="list" item="item" index="index" separator=","> (#{item.name},#{item.age},#{item.sex}) </foreach> </insert> 2、MyBatis ExecutorType.BATCH Mybatis内置ExecutorType,默认是simple,该模式下它为每个语句的执行创建一个新的预处理语句,单条提交sql。关注微信公众号Java精选,内涵视频资料、开源项目、源码分析等等。 batch模式会重复使用已经预处理的语句,并批量执行所有更新语句。但batch模式Insert操作时,在事务没有提交前,是无法获取到自增的id。 什么是自动装箱?什么是自动拆箱? 自动装箱是指将基本数据类型重新转化为对象。 public class Test { public static void main(String[] args) { Integer num = 9; } } num = 9的值是属于基本数据类型,原则上不能直接赋值给对象Integer。但是在JDK1.5版本后就可以进行这样的声明自动将基本数据类型转化为对应的封装类型,成为对象后可以调用对象所声明的方法。 自动拆箱是指将对象重新转化为基本数据类型。 public class Test { public static void main(String[] args) { // 声明Integer对象 Integer num = 9; // 隐含自动拆箱 System.out.print(num--); } } 由于对象不能直接进行运算,而是需要转化为基本数据类型后才能进行加减乘除。 // 装箱 Integer num = 10; // 拆箱 int num1 = num; 一级缓存和二级缓存有什么区别? 一级缓存 Mybatis一级缓存是指SQLSession 一级缓存的作用域是SQlSession Mabits默认开启一级缓存 同一个SqlSession中,执行相同的SQL查询时第一次会去查询数据库,并写在缓存中,第二次会直接从缓存中取。 当执行SQL时两次查询中间发生了增删改的操作,则SQLSession的缓存会被清空。 每次查询会先去缓存中找,如果找不到,再去数据库查询,然后把结果写到缓存中。 Mybatis的内部缓存使用一个HashMap,其中key为hashcode+statementId+sql语句,Value为查询出来的结果集映射成的java对象。 SqlSession执行insert、update、delete等操作commit后会清空该SQLSession缓存。 二级缓存 二级缓存是mapper级别的,Mybatis默认是没有开启二级缓存的。 第一次调用mapper下的SQL去查询用户的信息,查询到的信息会存放到mapper对应的二级缓存区域。 第二次调用namespace下的mapper映射文件中,相同的sql去查询用户信息,会去对应的二级缓存内取结果。 Redis 支持那些数据类型? String字符串 命令: set key value string类型是二进制安全的,它可以包含任何数据,如图片或序列化对象等。 string类型是Redis最基本的数据类型,一个键最大能存储512MB。 Hash(哈希) 命令: hmset name key1 value1 key2 value2 Redis hash是一个键值(key=>value)对集合。 Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象。 List(列表) Redis列表是简单的字符串列表,按照插入顺序排序。可以添加一个元素到列表的头部(左边)或者尾部(右边) 命令: lpush name value key对应list的头部添加字符串元素 命令: rpush name value key对应list的尾部添加字符串元素 命令: lrem name index key对应list中删除count个和value相同的元素 命令: llen name 返回key对应list的长度 Set(集合) 命令: sadd name value Redis的Set是string类型的无序集合。 集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。 zset(sorted set:有序集合) 命令: zadd name score value Redis zset和set一样也是string类型元素的集合,且不允许重复的成员。 不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。 zset的成员是唯一的,但分数(score)却可以重复。 什么是 Redis 持久化?Redis 有哪几种持久化方式? Redis持久化是指把内存的数据写到磁盘中去,防止服务因宕机导致内存数据丢失。 Redis提供了两种持久化方式:RDB(默认)和AOF。 RDB是Redis DataBase缩写,功能核心函数rdbSave(生成RDB文件)和rdbLoad(从文件加载内存)两个函数 AOF是Append-only file缩写,每当执行服务器(定时)任务或者函数时flushAppendOnlyFile函数都会被调用,这个函数执行以下两个工作。 AOF写入与保存 WRITE:根据条件,将aof_buf中的缓存写入到AOF文件 SAVE:根据条件,调用fsync或fdatasync函数,将AOF文件保存到磁盘中。 什么是缓存穿透?如何避免? 缓存穿透是指一般的缓存系统,都是按照key去缓存查询,如果不存在对应的value,就应该去后端系统查找(比如DB)。 一些恶意的请求会故意查询不存在的key,导致请求量大,造成后端系统压力大,这就是做缓存穿透。 如何避免? 1)对查询结果为空的情况也进行缓存,缓存时间设置短一点或key对应的数据insert后清理缓存。 2)对一定不存在的key进行过滤,可以把所有可能存在的key放到一个大的Bitmap中,查询时通过bitmap过滤。 什么是缓存雪崩?何如避免? 缓存雪崩是指当缓存服务器重启或大量缓存集中在某一个时间段内失效,这样在失效时,会给后端系统带来很大压力,导致系统崩溃。 如何避免? 1)在缓存失效后,使用加锁或队列的方式来控制读数据库写缓存的线程数量。如对某个key只允许一个线程查询数据和写缓存,其他线程等待。 2)实现二级缓存方式,A1为原始缓存,A2为拷贝缓存,A1失效时,切换访问A2,A1缓存失效时间设置为短期,A2设置为长期。 3)不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。 MyBatis 是否支持延迟加载?其原理是什么? Mybatis仅支持association关联对象和collection关联集合对象的延迟加载。 association指的就是一对一 collection指的就是一对多查询 在Mybatis配置文件中,启用延迟加载配置参数 lazyLoadingEnabled=true。 原理:使用CGLIB创建目标对象的代理对象,当调用目标方法时,进入拦截器方法。 比如调用a.getB().getName(),拦截器invoke()方法发现a.getB()是null值,就会单独发送事先保存好的查询关联B对象的SQL语句,先查询出B,然后再调用a.setB(b)赋值,最后再调用a.getB().getName()方法就有值了。几乎所有的包括Hibernate、Mybatis,支持延迟加载的原理都是一样的。 如何解决 MyBatis 转义字符的问题? xml配置文件使用转义字符 SELECT * FROM test WHERE crate_time &lt;= #{crate_time} AND end_date &gt;= #{crate_time} xml转义字符关系表 字符 转义 备注 < &lt; 小于号 > &gt; 大于号 & &amp; 和 ' &apos; 单引号 " &quot; 双引号 注意:XML中只有”<”和”&”是非法的,其它三个都是合法存在的,使用时都可以把它们转义了,养成一个良好的习惯。 转义前后的字符都会被xml解析器解析,为了方便起见,使用 <![CDATA[...]]> 来包含不被xml解析器解析的内容。标记所包含的内容将表示为纯文本,其中...表示文本内容。 但要注意的是: 1)此部分不能再包含”]]>”; 2)不允许嵌套使用; 3)”]]>”这部分不能包含空格或者换行。 Zookeeper 是如何保证事务的顺序一致性的? Zookeeper采用了全局递增的事务Id来标识,所有的proposal(提议)都在被提出的时候加上了zxid。 zxid实际上是一个64位的数字,高32位是epoch用来标识leader的周期。 如果有新的leader产生出来,epoch会自增,低32位用来递增计数。 当新产生proposal的时候,会依据数据库的两阶段过程,首先会向其他的server发出事务执行请求,如果超过半数的机器都能执行并且能够成功,那么就会开始执行。 Zookeeper 有哪几种部署模式? 部署模式:单机模式、伪集群模式、集群模式。 Zookeeper 集群最少要几台服务器,什么规则? 集群最少要3台服务器,集群规则为2N+1台,N>0,即3台。 Zookeeper 有哪些典型应用场景? Zookeeper是一个典型的发布/订阅模式的分布式数据管理与协调框架,开发者可以使用它来进行分布式数据的发布和订阅。 通过对Zookeeper中丰富的数据节点进行交叉使用,配合Watcher事件通知机制,可以非常方便的构建一系列分布式应用中年都会涉及的核心功能。应用场景如下: 1)数据发布/订阅 2)负载均衡 3)命名服务 4)分布式协调/通知 5)集群管理 6)Master选举 7)分布式锁 8)分布式队列 Paxos 和 ZAB 算法有什么区别和联系? 相同点 两者都存在一个类似于Leader进程的角色,由其负责协调多个Follower进程的运行; Leader进程都会等待超过半数的Follower做出正确的反馈后,才会将一个提案进行提交; ZAB协议中每个Proposal中都包含一个epoch值来代表当前的Leader周期,而Paxos中名字为Ballot。 不同点 Paxos是用来构建分布式一致性状态机系统,而ZAB用来构建高可用的分布式数据主备系统(Zookeeper)。 Zookeeper 中 Java 客户端都有哪些? Java客户端: 1)zk自带的zkclient 2)Apache开源的Curator Zookeeper 集群支持动态添加服务器吗? 动态添加服务器等同于水平扩容,而Zookeeper在这方面的表现并不是太好。两种方式: 1)全部重启 关闭所有Zookeeper服务,修改配置之后启动。不影响之前客户端的会话。 2)逐一重启 在过半存活即可用的原则下,一台机器重启不影响整个集群对外提供服务。这是比较常用的方式,在Zookeeper 3.5版本开始支持动态扩容。 Zookeeper 和 Nginx 的负载均衡有什么区别? Zookeeper的负载均衡是可以调控,而Nginx的负载均衡只是能调整权重,其他可控的都需要自己写Lua插件;但是Nginx的吞吐量比Zookeeper大很多,具体按业务应用场景选择用哪种方式。 Zookeeper 节点宕机如何处理? Zookeeper本身是集群模式,官方推荐不少于3台服务器配置。Zookeeper自身保证当一个节点宕机时,其他节点会继续提供服务。 假设其中一个Follower宕机,还有2台服务器提供访问,因为Zookeeper上的数据是有多个副本的,并不会丢失数据; 如果是一个Leader宕机,Zookeeper会选举出新的Leader。 Zookeeper集群的机制是只要超过半数的节点正常,集群就可以正常提供服务。只有Zookeeper节点挂得太多,只剩不到一半节点提供服务,才会导致Zookeeper集群失效。 Zookeeper集群节点配置原则 3个节点的cluster可以挂掉1个节点(leader可以得到2节 > 1.5)。 2个节点的cluster需保证不能挂掉任何1个节点(leader可以得到 1节 <=1)。 Socket 前后端通信是如何实现服务器集群? 假设有两台A服务器服务S1和B服务器服务S2,应用服务S0 首先你接收客户端(浏览器)请求的服务肯定是单点(一对一)连接,之后交给应用服务S0处理分配给两台服务器A和B上的服务S1和S2。 分配原则可以按一定的策略,如计数器等,按当前活跃连接数来决定分给哪台服务器。也可以更科学的按两台服务器实际处理的数据量来分配,因为有些连接可能一直空闲。 如果两台服务器上的服务通信正常且数据库能够承受压力,访问请求并不是太多的情况下,可以考虑使用数据库传递消息,反之可以考虑使用Redis缓存技术。 如果需要即时传递消息,在其中一个服务器上的服务S1查找不到,把消息发给另外一台服务器上的服务S2或把消息存储至数据库或缓存当中,然后通知其他服务有生产消息。类似MQ处理方式可参考ActiveMQ。 为什么要用 Redis 而不用 Map、Guava 做缓存? 缓存可以划分为本地缓存和分布式缓存。 以Java为例,使用自带Map或者Guava类实现的是本地缓存,主要特点是轻量以及快速,它们的生命周期会随着JVM的销毁而结束,且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。 使用Redis或Memcached等被称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,具有一致性,但是需要保持Redis或Memcached服务的高可用。 Redis 是单线程的吗?为什么这么快? Redis是单线程的,至于为什么这么快主要是因如下几方面: 1)Redis是纯内存数据库,一般都是简单的存取操作,线程占用的时间很多,时间的花费主要集中在IO上,所以读取速度快。 2)Redis使用的是非阻塞IO、IO多路复用,使用了单线程来轮询描述符,将数据库的开、关、读、写都转换成了事件,减少了线程切换时上下文的切换和竞争。 3)Redis采用了单线程的模型,保证了每个操作的原子性,也减少了线程的上下文切换和竞争。 4)Redis避免了多线程的锁的消耗。 5)Redis采用自己实现的事件分离器,效率比较高,内部采用非阻塞的执行方式,吞吐能力比较大 为什么使用消息队列? 消息队列中间件有很多,使用的场景广泛。主要解决的核心问题是削峰填谷、异步处理、模块解耦。 RabbitMQ 有几种广播类型? direct(默认方式):最基础最简单的模式,发送方把消息发送给订阅方,如果有多个订阅者,默认采取轮询的方式进行消息发送。 headers:与direct类似,只是性能很差,此类型几乎用不到。 fanout:分发模式,把消费分发给所有订阅者。 topic:匹配订阅模式,使用正则匹配到消息队列,能匹配到的都能接收到。 Kafka 的分区策略有哪些? ProducerRecord内部数据结构: -- Topic (名字) -- PartitionID ( 可选) -- Key[( 可选 ) -- Value 提供三种构造函数形参: ProducerRecord(topic, partition, key, value) ProducerRecord(topic, key, value) ProducerRecord(topic, value) 第一种分区策略:指定分区号,直接将数据发送到指定的分区中。 第二种分区策略:没有指定分区号,指定数据的key值,通过key取上hashCode进行分区。 第三种分区策略:没有指定分区号,也没有指定key值,直接轮循进行分区。 第四种分区策略:自定义分区。 RabbitMQ 有哪些重要组件? ConnectionFactory(连接管理器):应用程序与RabbitMQ之间建立连接的管理器 Channel(信道):消息推送使用的通道 Exchange(交换器):用于接受、分配消息 Queue(队列):用于存储生产者的消息 RoutingKey(路由键):用于把生产者的数据分配到交换器上 BindKey(绑定键):用于把交换器的消息绑定到队列上 RabbitMQ 有哪些重要角色? 生产者: 消息的生产者,负责创建和推送数据到消息服务器。 消费者: 消息的接收者,用于处理数据和确认消息。 代理: RabbitMQ本身,用于扮演传递消息的角色,本身并不生产消息。 RabbitMQ 如何保证消息顺序性? RabbitMQ保证消息的顺序性方式有两种: 1)拆分多个queue,每个queue对应一个consumer(消费者),就是多一些queue。 2)一个queue,对应一个consumer(消费者),然后这个consumer内部用内存队列做排队,然后分发给底层不同的worker来处理。 如何保证消息消费的幂等性? 1、利用数据库的唯一约束实现幂等 比如将订单表中的订单编号设置为唯一索引,创建订单时,根据订单编号就可以保证幂等 2、去重表 根据数据库的唯一性约束类似的方式来实现。其实现大体思路是:首先在去重表上建唯一索引,其次操作时把业务表和去重表放在同个本地事务中,如果出现重现重复消费,数据库会抛唯一约束异常,操作就会回滚。 3、利用redis的原子性 每次操作都直接set到redis里面,然后将redis数据定时同步到数据库中。 4、多版本(乐观锁)控制 此方式多用于更新的场景下。其实现的大体思路是:给业务数据增加一个版本号属性,每次更新数据前,比较当前数据的版本号是否和消息中的版本一致,如果不一致则拒绝更新数据,更新数据的同时将版本号+1。 5、状态机机制 此方式多用于更新且业务场景存在多种状态流转的场景。 6、token机制 生产者发送每条数据时,增加一个全局唯一ID,这个ID通常是业务的唯一标识。在消费者消费时,需要验证这个ID是否被消费过,如果没消费过,则进行业务处理。处理结束后,在把这个ID存入Redis,同时设置状态为已消费。如果已经消费过,则清除Redis缓存的这个ID。 Kafka 消费者如何取消订阅? 在KafkaConsumer类中使用unsubscribe()方法来取消主题的订阅。该方法可以取消如下的订阅方法: subscribe(Collection);方式实现的订阅 subscribe(Pattern);方式实现的订阅 assign() 方式实现的订阅 取消订阅使用代码如下: consumer.unsubscribe(); 将subscribe(Collection)或assign()中的集合参数设置为空集合,也可以实现取消订阅,以下三种方式都可以取消订阅: consumer.unsubscribe(); consumer.subscribe(new ArrayList<String>()); consumer.assign(new ArrayList<TopicPartition>()); 设计模式有多少种,都有哪些设计模式? Java有23种设计模式 设计模式总体分为三大类: 创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。 结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。 行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。 其实还有两类:并发型模式和线程池模式。 设计模式的六大原则是什么? 1)开闭原则(Open Close Principle) 开闭原则就是说对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。所以一句话概括就是:为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类,后面的具体设计中我们会提到这点。 2)里氏代换原则(Liskov Substitution Principle) 里氏代换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。 3)依赖倒转原则(Dependence Inversion Principle) 这个是开闭原则的基础,具体内容:真对接口编程,依赖于抽象而不依赖于具体。 4)接口隔离原则(Interface Segregation Principle) 大概意思是指使用多个隔离的接口,比使用单个接口要好。还是一个降低类之间的耦合度的意思,从这儿我们看出,其实设计模式就是一个软件的设计思想,从大型软件架构出发,为了升级和维护方便。所以上文中多次出现:降低依赖,降低耦合。 5)迪米特法则(最少知道原则)(Demeter Principle) 为什么叫最少知道原则,就是说:一个实体应当尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立。 6)合成复用原则(Composite Reuse Principle) 原则是尽量使用合成/聚合的方式,而不是使用继承。 什么是单例模式? 单例模式的定义就是确保某一个类仅有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。 单例模式分为:懒汉式单例、饿汉式单例、登记式单例三种。 单例模式特点: 1)单例类只能有一个实例。 2)单例类必须自己创建自己的唯一实例。 3)单例类必须给所有其他对象提供这一实例。 单例模式的优点是内存中只有一个对象,节省内存空间;避免频繁的创建销毁对象,可以提高性能;避免对资源的多重占用,简化访问;为整个系统提供一个全局访问点。 单例模式的缺点是不适用于变化频繁的对象;滥用单利将带来一些问题,如为了节省资源将数据库连接池对象设计为的单利类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为该对象是垃圾而被回收,这可能会导致对象状态的丢失。 从名字上来说饿汉和懒汉,饿汉就是类一旦加载,就把单例初始化完成,保证getInstance的时候单例是已经被初始化,而懒汉比较懒,只有当调用getInstance的时候,才回去初始化这个单例。 1、懒汉式单例,具体代码如下: package com.yoodb; //懒汉式单例类.在第一次调用的时候实例化自己 public class Singleton { //私有的无参构造方法 private Singleton(){ } //注意,这里没有final private static Singleton single = null; //静态工厂方法 public synchronized static Singleton getInstance(){ if(single == null){ single = new Singleton(); } return single; } } 2、饿汉式单例,具体代码如下: package com.yoodb; public class Singleton { //私有的无参构造方法 private Singleton(){ } //自动实例化 private final static Singleton single = new Singleton(); //静态工厂方法 public static synchronized Singleton getInstance(){ return single; } } 3、登记式单例,具体代码如下: package com.yoodb; import java.util.HashMap; import java.util.Map; public class Singleton { public static Map<String,Singleton> map = new HashMap<String,Singleton>(); static{ //将类名注入下次直接获取 Singleton single = new Singleton(); map.put(single.getClass().getName(),single); } //保护式无参构造方法 protected Singleton(){} public static Singleton getInstance(String name){ if(name == null){ name = Singleton.class.getName(); } if(map.get(name) == null){ try { map.put(name, Singleton.class.newInstance()); } catch (InstantiationException | IllegalAccessException e) { // TODO Auto-generated catch block e.printStackTrace(); } } return map.get(name); } public String create() { return "创建一个单例!"; } public static void main(String[] args) { Singleton single = Singleton.getInstance(null); System.out.println(single.create()); } } 单例模式中饿汉式和懒汉式有什么区别? 1、线程安全 饿汉式是线程安全的,可以直接用于多线程而不会出现问题,懒汉式就不行,它是线程不安全的,如果用于多线程可能会被实例化多次,失去单例的作用。 2、资源加载 饿汉式在类创建的同时就实例化一个静态对象,不管之后会不会使用这个单例,都会占据一定的内存资源,相应的在调用时速度也会更快。 懒汉式顾名思义,会延迟加载,在第一次使用该单例时才会实例化对象出来,第一次掉用时要初始化,如果要做的工作比较多,性能上会有些延迟,第一次调用之后就和饿汉式。 单例模式都有哪些应用场景? 1)需要生成唯一序列; 2)需要频繁实例化然后销毁代码的对象; 3)有状态的工具类对象; 4)频繁访问数据库或文件的对象。 例如:项目中读取配置文件、数据库连接池参数、spring中的bean默认是单例、线程池(threadpool)、缓存(cache)、日志参数等程序的对象。 需要注意的事这些场景只能有一个实例,如果生成多个实例,就会导致许多问题产生,如:程序行为异常、资源使用过量或数据不一致等。 什么是线程安全? 如果代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的且其他变量的值也和预期的是一样的,就是线程安全的。 或者也可以理解成一个类或程序所提供的接口对于线程来说是原子操作,多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说不用考虑同步的问题,那就是线程安全的。 Spring 框架中使用了哪些设计模式? Spring框架中使用大量的设计模式,下面列举比较有代表性的: 代理模式 AOP能够将那些与业务无关(事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度有利于可拓展性和可维护性。 单例模式 Spring中bean的默认作用域是单例模式,在Spring配置文件中定义bean默认为单例模式。 模板方法模式 模板方法模式是一种行为设计模式,用来解决代码重复的问题,如RestTemplate、JmsTemplate、JpaTemplate。 包装器设计模式 Spring根据不同的业务访问不同的数据库,能够动态切换不同的数据源。 观察者模式 Spring事件驱动模型就是观察者模式很经典的一个应用。 工厂模式 Spring使用工厂模式通过BeanFactory、ApplicationContext创建bean对象。 Spring MVC 执行流程是什么? 1)客户端发送请求到前端控制器DispatcherServlet。 2)DispatcherServlet收到请求调用HandlerMapping处理器映射器。 3)处理器映射器找到具体的处理器,根据xml配置、注解进行查找,生成处理器对象及处理器拦截器,并返回给DispatcherServlet。 4)DispatcherServlet调用HandlerAdapter处理器适配器。 5)HandlerAdapter经过适配调用具体的后端控制器Controller。 6)Controller执行完成返回ModelAndView。 7)HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet。 8)DispatcherServlet将ModelAndView传给ViewReslover视图解析器。 9)ViewReslover解析后返回具体View。 10)DispatcherServlet根据View进行渲染视图,将模型数据填充至视图中。 11)DispatcherServlet响应客户端。 组件说明 DispatcherServlet:作为前端控制器,整个流程控制的中心,控制其它组件执行,统一调度,降低组件之间的耦合性,提高每个组件的扩展性。 HandlerMapping:通过扩展处理器映射器实现不同的映射方式,例如:配置文件方式,实现接口方式,注解方式等。 HandlAdapter:通过扩展处理器适配器,支持更多类型的处理器。 ViewResolver:通过扩展视图解析器,支持更多类型的视图解析,例如:jsp、freemarker、pdf、excel等。 Spring MVC 如何解决请求中文乱码问题? 解决GET请求中文乱码问题 方式一:每次请求前使用encodeURI对URL进行编码。 方式二:在应用服务器上配置URL编码格式,在Tomcat配置文件server.xml中增加URIEncoding="UTF-8",然后重启Tomcat即可。 <ConnectorURIEncoding="UTF-8" port="8080" maxHttpHeaderSize="8192" maxThreads="150" minSpareThreads="25" maxSpareThreads="75"connectionTimeout="20000" disableUploadTimeout="true" URIEncoding="UTF-8" /> 解决POST请求中文乱码问题 方式一:在request解析数据时设置编码格式: request.setCharacterEncoding("UTF-8"); 方式二:使用Spring提供的编码过滤器 在web.xml文件中配置字符编码过滤器,增加如下配置: <!--编码过滤器--> <filter> <filter-name>encodingFilter</filter-name> <filter-class> org.springframework.web.filter.CharacterEncodingFilter </filter-class> <!-- 开启异步支持--> <async-supported>true</async-supported> <init-param> <param-name>encoding</param-name> <param-value>utf-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> 该过滤器的作用就是强制所有请求和响应设置编码格式: request.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8"); Spring MVC 请求转发和重定向有什么区别? 请求转发是浏览器发出一次请求,获取一次响应,而重定向是浏览器发出2次请求,获取2次请求。 请求转发是浏览器地址栏未发生变化,是第1次发出的请求,而重定向是浏览器地址栏发生变化,是第2次发出的请求。 请求转发称为服务器内跳转,重定向称为服务器外跳转。 请求转发是可以获取到用户提交请求中的数据,而重定向是不可以获取到用户提交请求中的数据,但可以获取到第2次由浏览器自动发出的请求中携带的数据。 请求转发是可以将请求转发到WEB-INF目录下的(内部)资源,而重定向是不可以将请求转发到WEB-INF目录下的资源。 Spring MVC 中系统是如何分层? 表现层(UI):数据的展现、操作页面、请求转发。 业务层(服务层):封装业务处理逻辑。 持久层(数据访问层):封装数据访问逻辑。 各层之间的关系:MVC是一种表现层的架构,表现层通过接口调用业务层,业务层通过接口调用持久层,当下一层发生改变,不影响上一层的数据。 如何开启注解处理器和适配器? 在配置文件中(一般命名为springmvc.xml 文件)通过开启配置: <mvc:annotation-driven> 来实现注解处理器和适配器的开启。 Spring MVC 如何设置重定向和转发? 在返回值前面加“forward:”参数,可以实现转发。 "forward:getName.do?name=Java精选" 在返回值前面加“redirect:”参数,可以实现重定向。 "redirect:https://blog.yoodb.com" Spring MVC 中函数的返回值是什么? Spring MVC的返回值可以有很多类型,如String、ModelAndView等,但事一般使用String比较友好。 @RequestMapping 注解用在类上有什么作用? @RequestMapping注解是一个用来处理请求地址映射的注解,可用于类或方法上。 用于类上,则表示类中的所有响应请求的方法都是以该地址作为父路径。 比如 ```java @Controller @RequestMapping("/test/") public class TestController{ } 启动的是本地服务,默认端口是8080,通过浏览器访问的路径就是如下地址: http://localhost:8080/test/ Spring MVC 控制器是单例的吗? 默认情况下是单例模式,在多线程进行访问时存在线程安全的问题。 解决方法可以在控制器中不要写成员变量,这是因为单例模式下定义成员变量是线程不安全的。 通过@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)或@Scope("prototype")可以实现多例模式。但是不建议使用同步,因为会影响性能。 使用单例模式是为了性能,无需频繁进行初始化操作,同时也没有必要使用多例模式。 RequestMethod 可以同时支持POST和GET请求访问吗? POST请求访问 @RequestMapping(value="/wx/getUserById",method = RequestMethod.POST) GET请求访问 @RequestMapping(value="/wx/getUserById/{userId}",method = RequestMethod.GET) 同时支持POST和GET请求访问 @RequestMapping(value="/subscribe/getSubscribeList/{customerCode}/{sid}/{userId}") RequestMethod常用的参数 GET(SELECT):从服务器查询,在服务器通过请求参数区分查询的方式。 POST(CREATE):在服务器新建一个资源,调用insert操作。 PUT(UPDATE):在服务器更新资源,调用update操作。 DELETE(DELETE):从服务器删除资源,调用delete语句。 Spring 依赖注入有几种实现方式? 1)Constructor构造器注入:通过将@Autowired注解放在构造器上来完成构造器注入,默认构造器参数通过类型自动装配。 public class Test { private UserInterface user; @Autowired private Test(UserInterface user) { this.user = user; } } 2)Field接口注入:通过将@Autowired注解放在构造器上来完成接口注入。 @Autowired //接口注入 private IUserService userService; 3)Setter方法参数注入:通过将@Autowired注解放在方法上来完成方法参数注入。 public class Test { private User user; @Autowired public void setUser(User user) { this.user = user; } public String getuser() { return user; } } Spring 可以注入null或空字符串吗? 完全可以 Spring 支持哪几种 bean 作用域? 1)singleton:默认,每个容器中只有一个bean的实例,单例的模式由BeanFactory自身来维护。 2)prototype:为每一个bean请求提供一个实例。 3)request:为每一个网络请求创建一个实例,在请求完成后,bean会失效并被垃圾回收器回收。 4)session:与request范围类似,确保每个session中有一个bean的实例,在session过期后,bean会随之失效。 5)global-session:全局作用域,global-session和Portlet应用相关。 当应用部署在Portlet容器中工作时,它包含很多portlet。 如果想要声明让所有的portlet共用全局的存储变量的话,那么这全局变量需要存储在global-session中。 全局作用域与Servlet中的session作用域效果相同。 JDK1.8 中 ConcurrentHashMap 不支持空键值吗? 首先明确一点HashMap是支持空键值对的,也就是null键和null值,而ConcurrentHashMap是不支持空键值对的。 查看一下JDK1.8源码,HashMap类部分源码,代码如下: public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; } static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } HashMap在调用put()方法存储数据时会调用hash()方法来计算key的hashcode值,可以从hash()方法上得出当key==null时返回值是0,这意思就是key值是null时,hash()方法返回值是0,不会再调用key.hashcode()方法。 ConcurrentHashMap类部分源码,代码如下: public V put(K key, V value) { return putVal(key, value, false); } /** Implementation for put and putIfAbsent */ final V putVal(K key, V value, boolean onlyIfAbsent) { if (key == null || value == null) throw new NullPointerException(); int hash = spread(key.hashCode()); int binCount = 0; for (Node<K,V>[] tab = table;;) { Node<K,V> f; int n, i, fh; if (tab == null || (n = tab.length) == 0) tab = initTable(); else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) break; // no lock when adding to empty bin } else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f); else { V oldVal = null; synchronized (f) { if (tabAt(tab, i) == f) { if (fh >= 0) { binCount = 1; for (Node<K,V> e = f;; ++binCount) { K ek; if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { oldVal = e.val; if (!onlyIfAbsent) e.val = value; break; } Node<K,V> pred = e; if ((e = e.next) == null) { pred.next = new Node<K,V>(hash, key, value, null); break; } } } else if (f instanceof TreeBin) { Node<K,V> p; binCount = 2; if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) { oldVal = p.val; if (!onlyIfAbsent) p.val = value; } } } } if (binCount != 0) { if (binCount >= TREEIFY_THRESHOLD) treeifyBin(tab, i); if (oldVal != null) return oldVal; break; } } } addCount(1L, binCount); return null; } ConcurrentHashmap在调用put()方法时调用了putVal()方法,而在该方法中判断key为null或value为null时抛出空指针异常NullPointerException。 ConcurrentHashmap是支持并发的,当通过get()方法获取对应的value值时,如果指定的键为null,则为NullPointerException,这主要是因为获取到的是null值,无法分辨是key没找到null还是有key值为null。 Spring 有哪些不同的通知类型? 通知(advice)是在程序中想要应用在其他模块中横切关注点的实现。 Advice主要有5种类型: @Before注解使用Advice 前置通知(BeforeAdvice):在连接点之前执行的通知(advice),除非它抛出异常,否则没有能力中断执行流。 @AfterReturning注解使用Advice 返回之后通知(AfterRetuningAdvice):如果一个方法没有抛出异常正常返回,在连接点正常结束之后执行的通知(advice)。 @AfterThrowing注解使用Advice 抛出(异常)后执行通知(AfterThrowingAdvice):若果一个方法抛出异常来退出的话,这个通知(advice)就会被执行。 @After注解使用Advice 后置通知(AfterAdvice):无论连接点是通过什么方式退出的正常返回或者抛出异常都会执行在结束后执行这些通知(advice)。 注解使用Advice 围绕通知(AroundAdvice):围绕连接点执行的通知(advice),只有这一个方法调用。这是最强大的通知(advice)。 Spring AOP 连接点和切入点是什么? 连接点(Joint Point) 连接点是指一个应用执行过程中能够插入一个切面的点,可以理解成一个方法的执行或者一个异常的处理等。 连接点可以是调用方法、抛出异常、修改字段等时,切面代码可以利用这些点插入到应用的正规流程中。使得程序执行过程中能够应用通知的所有点。 在Spring AOP中一个连接点总是代表一个方法执行。如果在这些方法上使用横切的话,所有定义在EmpoyeeManager接口中的方法都可以被认为是一个连接点。 切入点(Point cut) 切入点是一个匹配连接点的断言或者表达式,如果通知定义了“什么”和“何时”,那么切点就定义了“何处”。 通知(Advice)与切入点表达式相关联,切入点用于准确定位,确定在什么地方应用切面通知。 例如表达式 execution(* EmployeeManager.getEmployeeById(...)) 可以匹配EmployeeManager接口的getEmployeeById()。 Spring默认使用AspectJ切入点表达式,由切入点表达式匹配的连接点概念是AOP的核心。 Spring AOP 代理模式是什么? 代理模式是使用非常广泛的设计模式之一。 代理模式是指给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。通俗的来讲代理模式就是生活中常见的房产中介。 Spring AOP是基于代理实现的。 Spring AOP代理是一个由AOP框架创建的用于在运行时实现切面协议的对象。 Spring AOP默认为AOP代理使用标准的JDK动态代理,这使得任何接口(或者接口的集合)可以被代理。 Spring AOP也可以使用CGLIB代理,如果业务对象没有实现任何接口那么默认使用CGLIB。 Spring 框架有哪些特点? 1)方便解耦,简化开发 通过Spring提供的IoC容器,可以将对象之间的依赖关系交由Spring进行控制,避免编码所造成的过度耦合。使用Spring可以使用户不必再为单实例模式类、属性文件解析等底层的需求编码,可以更专注于上层的业务逻辑应用。 2)支持AOP面向切面编程 通过Spring提供的AOP功能,方便进行面向切面的编程,许多不容易用传统OOP实现的功能可以通过AOP轻松完成。 3)支持声明事物 通过Spring可以从单调繁琐的事务管理代码中解脱出来,通过声明式方式灵活地进行事务的管理,提高开发效率和质量。 4)方便程序测试 使用非容器依赖的编程方式进行几乎所有的测试工作,通过Spring使得测试不再是高成本的操作,而是随手可做的事情。Spring对Junit4支持,可以通过注解方便的测试Spring程序。 5)方便便捷集成各种中间件框架 Spring可以降低集成各种中间件框架的难度,Spring提供对各种框架如Struts,Hibernate、Hessian、Quartz等的支持。 6)降低Java EE API使用难度 Spring对很多Java EE API如JDBC、JavaMail、远程调用等提供了一个简易的封装层,通过Spring的简易封装,轻松实现这些Java EE API的调用。 Spring 是由哪些模块组成的? Core module Bean module Context module Expression Language module JDBC module ORM module OXM module Java Messaging Service(JMS) module Transaction module Web module Web-Servlet module Web-Struts module Web-Portlet module Spring 提供几种配置方式设置元数据? Spring配置元数据可以采用三种方式,可以混合使用。 1)基于XML的配置元数据 使用XML文件标签化配置Bean的相关属性。 属性 描述 对应注解 class 此项必填,指定要创建Bean的类(全路径) 无 id 全局唯一 指定bean的唯一标示符 无 name 全局唯一 指定bean的唯一标示符 @Bean的name属性 scope 创建bean的作用域 @Scope singleton 是否单例 @Scope(value=SCOPE_SINGLETON) depends-on 用来表明依赖关系 @DependsOn depends-check 依赖检查 无 autowire 自动装配 默认NO @Bean的autowire属性 init-method 对象初始化后调用的方法 @Bean 的initMethod属性 destroy-method 对象销毁前调用的方法 @Bean 的destroyMethod lazy-init 容器启动时不会初始化,只有使用时初始化 @Lazy primary 容器中有多个相同类型的bean时,autowired时优先使用primary=true @Primary factory-method 工厂创建对象的方法 无 factory-bean 工厂bean 无 2)基于注解的配置元数据 @Component 标识一个被Spring管理的对象 @Respository 标识持久层对象 @Service 标识业务层对象 @Controller 标识表现层对象 Spring Boot项目中Contrller层、Service层、Dao层均采用注解的方式,在对应类上加相对应的@Controller,@Service,@Repository,@Component等注解。 需要注意的是使用@ComponentScan注解,若是Spring Boot框架项目,扫描组件的默认路径与Application.class同级包。 3)基于Java的配置元数据 默认情况下,方法名即为Bean名称。Java使用Configuration类配置bean,</bean>对应Spring注解@Bean。 目前常用的方式是第二种和第三种,也经常结合使用。 HTTP1.0 和 HTTP1.1 有什么区别? 1)长连接(Persistent Connection) HTTP1.0规定浏览器与服务器只保持短暂的连接,浏览器的每次请求都需要与服务器建立一个TCP连接,服务器完成请求处理后立即断开TCP连接,服务器不跟踪每个客户也不记录过去的请求。 HTTP1.1支持长连接,在请求头中有Connection:Keep-Alive。在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟。 2)节省带宽 HTTP1.0中存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象传输过去,并且不支持断点续传功能。 HTTP1.1支持只发送header信息,不携带其他任何body信息,如果服务器认为客户端有权限请求服务器,则返回100状态码,客户端接收到100状态码后把请求body发送到服务器;如果返回401状态码,客户端无需发送请求body节省带宽。 3)HOST域 HTTP1.0没有host域,HTTP1.0中认为每台服务器都绑定一个唯一的IP地址,因此,请求消息中的URL并没有传递主机名(hostname)。 HTTP1.1的请求消息和响应消息都支持host域,且请求消息中若是host域会报告400 Bad Request错误。一台物理服务器上可以同时存在多个虚拟主机(Multi-homed Web Servers),并且它们可以共享一个IP地址。 4)缓存处理 HTTP1.0中主要使用header里的If-Modified-Since,Expires来做为缓存判断的标准。 HTTP1.1引入了更多的缓存控制策略如Entity tag、If-Unmodified-Since、If-Match、If-None-Match等更多可供选择的缓存头来控制缓存策略。 5)错误通知管理 HTTP1.1中新增24个错误状态响应码,比如409(Conflict)表示请求的资源与资源的当前状态发生冲突;410(Gone)表示服务器上的某个资源被永久性的删除。 HTTP1.1 和 HTTP2.0 有什么区别? 1)多路复用 HTTP2.0使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比HTTP1.1大了好几个数量级。 HTTP1.1可以建立多个TCP连接来支持处理更多并发的请求,但是创建TCP连接本身也是有开销的。 2)头部数据压缩 HTTP1.1中HTTP请求和响应都是由状态行、请求/响应头部、消息主体三部分组成。 一般而言,消息主体都会经过gzip压缩或本身传输的就是压缩过后的二进制文件,但状态行和头部却没有经过任何压缩,直接以纯文本传输。 随着Web功能越来越复杂,每个页面产生的请求数也越来越多,导致消耗在头部的流量越来越多,尤其是每次都要传输UserAgent、Cookie等不会频繁变动的内容,完全是一种浪费资源的体现。 HTTP1.1不支持header数据的压缩,而HTTP2.0使用HPACK算法对header的数据进行压缩,压缩的数据体积小,在网络上传输更快。 3)服务器推送 服务端推送是一种在客户端请求前发送数据的机制。 网页中使用了许多资源:HTML、样式表、脚本、图片等,在HTTP1.1中这些资源每一个都必须明确地请求,这是一个很慢的过程。 浏览器从获取HTML开始,然后在它解析和评估页面时获取更多的资源,因为服务器必须等待浏览器做每一个请求,网络经常是空闲和未充分使用的。 HTTP2.0引入了server push,允许服务端推送资源给浏览器,在浏览器明确请求前,不用客户端再次创建连接发送请求到服务器端获取,客户端可以直接从本地加载这些资源,不用再通过网络。 Spring Boot 支持哪几种内嵌容器? Spring Boot支持的内嵌容器有Tomcat(默认)、Jetty、Undertow和Reactor Netty(v2.0+),借助可插拔(SPI)机制的实现,开发者可以轻松进行容器间的切换。 什么是 Spring Boot Stater? Spring Boot在配置上相比Spring要简单许多,其核心在于Spring Boot Stater。 Spring Boot内嵌容器支持Tomcat、Jetty、Undertow等应用服务的starter启动器,在应用启动时被加载,可以快速的处理应用所需要的一些基础环境配置。 starter解决的是依赖管理配置复杂的问题,可以理解成通过pom.xml文件配置很多jar包组合的maven项目,用来简化maven依赖配置,starter可以被继承也可以依赖于别的starter。 比如spring-boot-starter-web包含以下依赖: org.springframework.boot:spring-boot-starter org.springframework.boot:spring-boot-starter-tomcat org.springframework.boot:spring-boot-starter-validation com.fasterxml.jackson.core:jackson-databind org.springframework:spring-web org.springframework:spring-webmvc starter负责配与Sping整合相关的配置依赖等,使用者无需关心框架整合带来的问题。 比如使用Sping和JPA访问数据库,只需要项目包含spring-boot-starter-data-jpa依赖就可以完美执行。 Spring Boot Stater 有什么命名规范? 1)Spring Boot官方项目命名方式 前缀:spring-boot-starter-,其中是指定类型的应用程序名称。 格式:spring-boot-starter-{模块名} 举例:spring-boot-starter-web、spring-boot-starter-jdbc 2)自定义命名方式 后缀:*-spring-boot-starter 格式:{模块名}-spring-boot-starter 举例:mybatis-spring-boot-starter Spring Boot 启动器都有哪些? Spring Boot在org.springframework.boot组下提供了以下应用程序启动器: 名称 描述 spring-boot-starter 核心启动器,包括自动配置支持,日志记录和YAML spring-boot-starter-activemq 使用Apache ActiveMQ的JMS消息传递启动器 spring-boot-starter-amqp 使用Spring AMQP和Rabbit MQ的启动器 spring-boot-starter-aop 使用Spring AOP和AspectJ进行面向方面编程的启动器 spring-boot-starter-artemis 使用Apache Artemis的JMS消息传递启动器 spring-boot-starter-batch 使用Spring Batch的启动器 spring-boot-starter-cache 使用Spring Framework的缓存支持的启动器 spring-boot-starter-data-cassandra 使用Cassandra分布式数据库和Spring Data Cassandra的启动器 spring-boot-starter-data-cassandra-reactive 使用Cassandra分布式数据库和Spring Data Cassandra Reactive的启动器 spring-boot-starter-data-couchbase 使用Couchbase面向文档的数据库和Spring Data Couchbase的启动器 spring-boot-starter-data-couchbase-reactive 使用Couchbase面向文档的数据库和Spring Data Couchbase Reactive的启动器 spring-boot-starter-data-elasticsearch 使用Elasticsearch搜索和分析引擎以及Spring Data Elasticsearch的启动器 spring-boot-starter-data-jdbc 使用Spring Data JDBC的启动器 spring-boot-starter-data-jpa 将Spring Data JPA与Hibernate结合使用的启动器 spring-boot-starter-data-ldap 使用Spring Data LDAP的启动器 spring-boot-starter-data-mongodb 使用MongoDB面向文档的数据库和Spring Data MongoDB的启动器 spring-boot-starter-data-mongodb-reactive 使用MongoDB面向文档的数据库和Spring Data MongoDB Reactive的启动器 spring-boot-starter-data-neo4j 使用Neo4j图形数据库和Spring Data Neo4j的启动器工具 spring-boot-starter-data-r2dbc 使用Spring Data R2DBC的启动器 spring-boot-starter-data-redis 使用Redis键值数据存储与Spring Data Redis和Lettuce客户端的启动器 spring-boot-starter-data-redis-reactive 将Redis键值数据存储与Spring Data Redis Reacting和Lettuce客户端一起使用的启动器 spring-boot-starter-data-rest 使用Spring Data REST在REST上公开Spring数据存储库的启动器 spring-boot-starter-freemarker 使用FreeMarker视图构建MVC Web应用程序的启动器 spring-boot-starter-groovy-templates 使用Groovy模板视图构建MVC Web应用程序的启动器 spring-boot-starter-hateoas 使用Spring MVC和Spring HATEOAS构建基于超媒体的RESTful Web应用程序的启动器 spring-boot-starter-integration 使用Spring Integration的启动器 spring-boot-starter-jdbc 结合使用JDBC和HikariCP连接池的启动器 spring-boot-starter-jersey 使用JAX-RS和Jersey构建RESTful Web应用程序的启动器。的替代品spring-boot-starter-web spring-boot-starter-jooq 使用jOOQ访问SQL数据库的启动器。替代spring-boot-starter-data-jpa或spring-boot-starter-jdbc spring-boot-starter-json 读写JSON启动器 spring-boot-starter-jta-atomikos 使用Atomikos的JTA交易启动器 spring-boot-starter-mail 使用Java Mail和Spring Framework的电子邮件发送支持的启动器 spring-boot-starter-mustache 使用Mustache视图构建Web应用程序的启动器 spring-boot-starter-oauth2-client 使用Spring Security的OAuth2 / OpenID Connect客户端功能的启动器 spring-boot-starter-oauth2-resource-server 使用Spring Security的OAuth2资源服务器功能的启动器 spring-boot-starter-quartz 启动器使用Quartz Scheduler spring-boot-starter-rsocket 用于构建RSocket客户端和服务器的启动器 spring-boot-starter-security 使用Spring Security的启动器 spring-boot-starter-test 用于使用包括JUnit Jupiter,Hamcrest和Mockito在内的库测试Spring Boot应用程序的启动器 spring-boot-starter-thymeleaf 使用Thymeleaf视图构建MVC Web应用程序的启动器 spring-boot-starter-validation 初学者,可将Java Bean验证与Hibernate Validator结合使用 spring-boot-starter-web 使用Spring MVC构建Web(包括RESTful)应用程序的启动器。使用Tomcat作为默认的嵌入式容器 spring-boot-starter-web-services 使用Spring Web Services的启动器 spring-boot-starter-webflux 使用Spring Framework的反应式Web支持构建WebFlux应用程序的启动器 spring-boot-starter-websocket 使用Spring Framework的WebSocket支持构建WebSocket应用程序的启动器 除应用程序启动器外,以下启动程序还可用于添加生产环境上线功能: |名称|描述| |-|-| |spring-boot-starter-actuator|使用Spring Boot Actuator的程序,该启动器提供了生产环境上线功能,可帮助您监视和管理应用程序| Spring Boot还包括以下启动程序,如果想排除或替换启动器,可以使用这些启动程序: |名称|描述| |-|-| |spring-boot-starter-jetty|使用Jetty作为嵌入式servlet容器的启动器。替代spring-boot-starter-tomcat| |spring-boot-starter-log4j2|使用Log4j2进行日志记录的启动器。替代spring-boot-starter-logging| |spring-boot-starter-logging|使用Logback进行日志记录的启动器。默认记录启动器| |spring-boot-starter-reactor-netty|启动器,用于将Reactor Netty用作嵌入式反应式HTTP服务器。| |spring-boot-starter-tomcat|启动器,用于将Tomcat用作嵌入式servlet容器。默认使用的servlet容器启动器spring-boot-starter-web| |spring-boot-starter-undertow|使用Undertow作为嵌入式servlet容器的启动器。替代spring-boot-starter-tomcat| Spring Cloud 断路器的作用是什么? 分布式架构中断路器模式的作用基本类似的,当某个服务单元发生故障,类似家用电器发生短路后,通过断路器的故障监控,类似熔断保险丝,向调用方返回一个错误响应,不需要长时间的等待。这样就不会出现因被调用的服务故障,导致线程长时间占用而不释放,避免了在分布式系统中故障的蔓延。 Spring Cloud 核心组件有哪些? Eureka:服务注册与发现,Eureka服务端称服务注册中心,Eureka客户端主要处理服务的注册与发现。 Feign:基于Feign的动态代理机制,根据注解和选择的机器,拼接请求url地址,发起请求。 Ribbon:负载均衡,服务间发起请求时基于Ribbon实现负载均衡,从一个服务的多台机器中选择一台。 Hystrix:提供服务隔离、熔断、降级机制,发起请求是通过Hystrix提供的线程池,实现不同服务调用之间的隔离,避免服务雪崩问题。 Zuul:服务网关,前端调用后端服务,统一由Zuul网关转发请求给对应的服务。 Spring Cloud 如何实现服务的注册? 1、当服务发布时,指定对应的服务名,将服务注册到注册中心,比如Eureka、Zookeeper等。 2、注册中心加@EnableEurekaServer注解,服务使用@EnableDiscoveryClient注解,然后使用Ribbon或Feign进行服务直接的调用发现。 服务发现组件的功能 1)服务注册表 服务注册表是一个记录当前可用服务实例的网络信息的数据库,是服务发现机制的核心。 服务注册表提供查询API和管理API,使用查询API获得可用的服务实例,使用管理API实现注册和注销; 2)服务注册 3)健康检查 什么是 Spring Cloud Config? Spring Cloud Config为分布式系统中的外部配置提供服务器和客户端支持,可以方便的对微服务各个环境下的配置进行实时更新、集中式管理。 Spring Cloud Config分为Config Server和Config Client两部分。 Config Server负责读取配置文件,并且暴露Http API接口,Config Client通过调用Config Server的接口来读取配置文件。 Spring Cloud Eureka 自我保护机制是什么? 1)假设某一时间某个微服务宕机了,而Eureka不会自动清除,依然对微服务的信息进行保存。 2)在默认的情况系,Eureka Server在一定的时间内没有接受到微服务的实例心跳(默认为90秒),Eureka Server将会注销该实例。 3)但是在网络发生故障时微服务在Eureka Server之间是无法通信的,因为微服务本身实例是健康的,此刻本不应该注销这个微服务。那么Eureka自我保护模式就解决了这个问题。 4)当Eureka Server节点在短时间内丢失过客户端时包含发生的网络故障,那么节点就会进行自我保护。 5)一但进入自我保护模式,Eureka Server就会保护服务注册表中的信息,不再删除服务注册表中的数据注销任何微服务。 6)当网络故障恢复后,这个Eureka Server节点会自动的退出自我保护机制。 7)在自我保护模式中Eureka Server会保护服务注册表中的信息,不在注销任何服务实例,当重新收到心跳恢复阀值以上时,这个Eureka Server节点就会自动的退出自我保护模式,这种设计模式是为了宁可保留错误的服务注册信息,也不盲目的注销删除任何可能健康的服务实例。 8)自我保护机制就是一种对网络异常的安全保护实施,它会保存所有的微服务,不管健康或不健康的微服务都会保存,而不会盲目的删除任何微服务,可以让Eureka集群更加的健壮、稳定。 9)在Eureka Server端可取消自我保护机制,但是不建议取消此功能。 常用的并发工具类有哪些? CountDownLatch闭锁 CountDownLatch是一个同步计数器,初始化时传入需要计数线程的等待数,可能是等于或大于等待执行完的线程数。调用多个线程之间的同步或说起到线程之间的通信(不是互斥)一组线程等待其他线程完成工作后在执行,相当于加强的join。 CyclicBarrier栅栏 CyclicBarrier字面意思是栅栏,是多线程中重要的类,主要用于线程之间互相等待的问题,初始化时传入需要等待的线程数。 作用:让一组线程达到某个屏障被阻塞直到一组内最后一个线程达到屏蔽时,屏蔽开放,所有被阻塞的线程才会继续运行。 Semophore信号量 semaphore称为信号量是操作系统的一个概念,在Java并发编程中,信号量控制的是线程并发的数量。 作用:semaphore管理一系列许可每个acquire()方法阻塞,直到有一个许可证可以获得,然后拿走许可证,每个release方法增加一个许可证,这可能会释放一个阻塞的acquire()方法,然而并没有实际的许可保证这个对象,semaphore只是维持了一个可获取许可的数量,主要控制同时访问某个特定资源的线程数量,多用在流量控制。 Exchanger交换器 Exchange类似于交换器可以在队中元素进行配对和交换线程的同步点,用于两个线程之间的交换。 具体来说,Exchanger类允许两个线程之间定义同步点,当两个线程达到同步点时,它们交换数据结构,因此第一个线程的数据结构进入到第二个线程当中,第二个线程的数据结构进入到第一个线程当中。 并发和并行有什么区别? 并行(parallellism)是指两个或者多个事件在同一时刻发生,而并发(parallellism)是指两个或多个事件在同一时间间隔发生。 并行是在不同实体上的多个事件,而并发是在同一实体上的多个事件。 并行是在一台处理器上同时处理多个任务(Hadoop分布式集群),而并发在多台处理器上同时处理多个任务。 JSP 模版引擎如何解析 ${} 表达式? 目前开发中已经很少使用JSP模版引擎,JSP虽然是一款功能比较强大的模板引擎,并被广大开发者熟悉,但它前后端耦合比较高。 其次是JSP页面的效率没有HTML高,因为JSP是同步加载。而且JSP需要Tomcat应用服务器部署,但不支持Nginx等,已经快被时代所淘汰。 JSP页面中使用${表达式}展示数据,但是页面上并没有显示出对应数据,而是把${表达式}当作纯文本显示。 原因分析:这是由于jsp模版引擎默认会无视EL表达式,需要手动设置igNoreEL为false。 <%@ page isELIgnored="false" %> 什么是服务熔断?什么是服务降级? 熔断机制 熔断机制是应对雪崩效应的一种微服务链路保护机制。 当某个微服务不可用或者响应时间过长时会进行服务降级,进而熔断该节点微服务的调用,快速返回“错误”的响应信息。 当检测到该节点微服务调用响应正常后恢复调用链路。 在Spring Cloud框架里熔断机制通过Hystrix实现,Hystrix会监控微服务间调用的状况,当失败的调用到一定阈值,缺省是5秒内调用20次,如果失败,就会启动熔断机制。 服务降级 服务降级一般是从整体负荷考虑,当某个服务熔断后,服务器将不再被调用,此时客户端可以准备一个本地fallback回调,返回一个缺省值。这样做目的是虽然水平下降,但是是可以使用,相比直接挂掉要强很多。 Spring Boot 和 Spring Cloud 之间有什么联系? Spring Boot是Spring推出用于解决传统框架配置文件冗余,装配组件繁杂的基于Maven的解决方案,旨在快速搭建单个微服务。 Spring Cloud专注于解决各个微服务之间的协调与配置,整合并管理各个微服务,为各个微服务之间提供配置管理、服务发现、断路器、路由、事件总线等集成服务。 Spring Cloud是依赖于Spring Boot,而Spring Boot并不依赖与Spring Cloud。 Spring Boot专注于快速、方便的开发单个的微服务个体,Spring Cloud是关注全局的服务治理框架。 你都知道哪些微服务技术栈? 微服务描述 技术名称 服务开发 Springboot、Spring、SpringMVC 服务配置与管理 Netflix公司的Archaius、阿里的Diamond等 服务注册与发现 Eureka、Consul、Zookeeper等 服务调用 REST、RPC、gRPC 服务熔断器 Hystrix、Envoy等 负载均衡 Ribbon、Nginx等 服务接口调用(客户端调用服务发简单工具) Feign等 消息队列 kafka、RabbitMQ、ActiveMQ等 服务配置中心管理 SpringCloudConfig、Chef等 服务路由(API网关) Zuul等 服务监控 Zabbix、Nagios、Metrics、Spectator等 全链路追踪 Zipkin、Brave、Dapper等 服务部署 Docker、OpenStack、Kubernetes等 数据流操作开发包 SpringCloud Stream(封装与Redis,Rabbit、Kafka等发送接收消息) 事件消息总线 Spring Cloud Bus 接口和抽象类有什么区别? 比较方面 抽象类说明 接口说明 默认方法 抽象类可以有默认的方法实现 JDK1。8之前版本,接口中不存在方法的实现 实现方式 子类使用extends关键字来继承抽象类。如果子类不是抽象类,子类需要提供抽象类中所声明方法的实现 子类使用implements来实现接口,需要提供接口中所有声明的实现。 构造器 抽象类中可以有构造器 接口中不能 和正常类区别 抽象类不能被实例化 接口则是完全不同的类型 访问修饰符 抽象方法可以有public、protected、default等修饰 接口默认是public,不能使用其他修饰符 多继承 一个子类只能存在一个父类 一个子类可以存在多个接口 添加新方法 抽象类中添加新方法,可以提供默认的实现,因此可以不修改子类现有的代码 如果往接口中添加新方法,则子类中需要实现该方法 什么是线程死锁? 线程死锁是指多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。 产生死锁必须具备以下四个条件: 互斥条件:该资源任意一个时刻只由一个线程占用; 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源持有不释放; 不剥夺条件:线程已获得的资源,在末使用完之前不能被其他线程强行剥夺,只有使用完毕后才释放资源; 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。 如何避免线程死锁? 只需要破坏产生死锁的四个条件中任意一个就可以避免线程死锁,但是互斥条件是没有办法破坏的,因为锁的意义就是想让线程之间存在资源互斥访问。 1)破坏请求与保持条件,一次性申请所有的资源; 2)破坏不剥夺条件,占用部分资源的线程进一步申请其他资源时如果申请不到,使其主动释放占有的资源; 3)破坏循环等待条件,按序申请资源来预防线程死锁,按某一顺序申请资源,释放资源则反序释放。 父类中静态方法能否被子类重写? 父类中静态方法不能被子类重写。 重写只适用于实例方法,不能用于静态方法,而且子类当中含有和父类相同签名的静态方法,一般称之为隐藏。 public class A { public static String a = "这是父类静态属性"; public static String getA() { return "这是父类静态方法"; } } public class B extends A{ public static String a = "这是子类静态属性"; public static String getA() { return "这是子类静态方法"; } public static void main(String[] args) { A a = new B(); System.out.println(a.getA()); } } 如上述代码所示,如果能够被重写,则输出的应该是“这是子类静态方法”。与此类似的是,静态变量也不能被重写。如果想要调用父类的静态方法,应该使用类来直接调用。 什么是不可变对象?有什么好处? 不可变对象是指对象一旦被创建,状态就不能再改变,任何修改都会创建一个新的对象。 比如String、Integer及其它包装类。 不可变对象最大的好处是线程安全。 静态变量和实例变量有什么区别? 静态变量:独立存在的变量,只是位置放在某个类下,可以直接类名加点调用静态变量名使用。并且是项目或程序一启动运行到该类时就直接常驻内存。不需要初始化类再调用该变量。用关键字static声明。静态方法也是同样,可以直接调用。 实例变量:相当于该类的属性,需要初始化这个类后才可以调用。如果这个类未被再次使用,垃圾回收器回收后这个实例也将不存在了,下次再使用时还需要初始化这个类才可以再次调用。 1)存储区域不同:静态变量存储在方法区属于类所有,实例变量存储在堆当中; 2)静态变量与类相关,普通变量则与实例相关; 3)内存分配方式不同。 4)生命周期不同。 需要注意的是从JDK1.8开始用于实现方法区的PermSpace被MetaSpace取代。 Object 类都有哪些公共方法? 1)equals(obj); 判断其他对象是否“等于”此对象。 2)toString(); 表示返回对象的字符串。通常,ToString方法返回一个“以文本方式表示”此对象的字符串。结果应该是一个简洁但信息丰富的表达,很容易让人阅读。建议所有子类都重写此方法。 **3)getClass(); ** 返回此对象运行时的类型。 4)wait(); 表示当前线程进入等待状态。 5)finalize(); 用于释放资源。 6)notify(); 唤醒在该对象上等待的某个线程。 7)notifyAll(); 唤醒在该对象上等待的所有线程。 8)hashCode(); 返回对象的哈希代码值。用于哈希查找,可以减少在查找中使用equals的次数,重写equals方法一般都要重写hashCode方法。这个方法在一些具有哈希功能的Collection中用到。 9)clone(); 实现对象的浅复制,实现Cloneable接口才可以调用这个方法,否则抛出CloneNotSupportedException异常。 Java 创建对象有哪几种方式? 创建对象 构造方法说明 使用new关键字 调用构造方法 使用Class类的newInstance方法 调用构造方法 使用Constructor类的newInstance方法 调用构造方法 使用clone方法 没有调用构造方法 使用反序列化 没有调用构造方法 a==b 与 a.equals(b) 有什么区别? a==b 与 a.equals(b) 有什么区别? 假设a和b都是对象 a==b是比较两个对象内存地址,当a和b指向的是堆中的同一个对象才会返回true。 a.equals(b)是比较的两个值内容,其比较结果取决于equals()具体实现。 多数情况下需要重写这个方法,如String类重写equals()用于比较两个不同对象,但是包含的字母相同的比较: public boolean equals(Object obj) { if (this == obj) {// 相同对象直接返回true return true; } if (obj instanceof String) { String anotherString = (String)obj; int n = value.length; if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; } return true; } } return false; } Object 中 equals() 和 hashcode() 有什么联系? Java的基类Object提供了一些方法,其中equals()方法用于判断两个对象是否相等,hashCode()方法用于计算对象的哈希码。equals()和hashCode()都不是final方法,都可以被重写(overwrite)。 hashCode()方法是为对象产生整型的hash值,用作对象的唯一标识。 hashCode()方法常用于基于hash的集合类,如Hashtable、HashMap等,根据Java规范使用equal()方法来判断两个相等的对象,必须具有相同的hashcode。 将对象放入到集合中时,首先要判断放入对象的hashcode是否已经存在,不存在则直接放入集合。 如果hashcode相等,然后通过equal()方法判断要放入对象与集合中的其他对象是否相等,使用equal()判断不相等,则直接将该元素放入集合中,反之不放入集合中。 hashcode() 中可以使用随机数字吗? hashcode()中不可以使用随机数字不行,这是因为对象的hashcode值必须是相同的。 Java 中 & 和 && 有什么区别? &是位操作 &&是逻辑运算符 逻辑运算符具有短路特性,而&不具备短路特性。 来看一下代码执行结果: public class Test{ static String name; public static void main(String[] args){ if(name!=null & name.equals("")){ System.out.println("ok"); }else{ System.out.println("error"); } } } 执行结果: Exception in thread "main" java.lang.NullPointerException at com.jingxuan.JingXuanApplication.main(JingXuanApplication.java:25) 上述代码执行时抛出空指针异常,若果&替换成&&,则输出日志是error。 一个 .java 类文件中可以有多少个非内部类? 一个.java类文件中只能出现一个public公共类,但是可以有多个default修饰的类。如果存在两个public修饰的类时,会报如下错误: The public type Test must be defined in its own file Java 中如何正确退出多层嵌套循环? 1)使用lable标签和break方式; lable是跳出循环标签。 break lable;是跳出循环语句。 当执行跳出循环语句时会跳出循环标签下方循环的末尾后面。 lable: for(int i=0;i<3;i++){ for(int j=0;j<3;j++){ System.out.println(i); if(i == 2) { break lable; } } } 上述代码在执行过程中,当i=2时,执行跳出循环语句,控制台只输出i=0和i=1的结果,执行继续for循环后面的代码。 0 0 0 1 1 1 2 执行for后面的程序代码 2)通过在外层循环中添加标识符,比如定义布尔类型bo = false,当bo=true跳出循环语句。 浅拷贝和深拷贝有什么区别? 浅拷贝是指被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。换言之,浅拷贝仅仅复制所考虑的对象,而不复制它所引用的对象。 深拷贝是指被复制对象的所有变量都含有与原来的对象相同的值,而那些引用其他对象的变量将指向被复制过的新对象,并且不再是原有的那些被引用的对象。换言之,深拷贝把要复制的对象所引用的对象都复制了一遍。 Java 中 final关键字有哪些用法? Java代码中被final修饰的类不可以被继承。 Java代码中被final修饰的方法不可以被重写。 Java代码中被final修饰的变量不可以被改变,如果修饰引用类型,那么表示引用类型不可变,引用类型指向的内容可变。 Java代码中被final修饰的方法,JVM会尝试将其内联,以提高运行效率。 Java代码中被final修饰的常量,在编译阶段会存入常量池中。 String s = new String("abc"); 创建了几个String对象? String s = new String("abc"); 创建了2个String对象。 一个是字符串字面常数,在字符串常量池中。 一个是new出来的字符串对象,在堆中。 String 和 StringBuffer 有什么区别? String是不可变对象,每次对String类型进行操作都等同于产生了一个新的String对象,然后指向新的String对象。因此尽量避免对String进行大量拼接操作,否则会产生很多临时对象,导致GC开始工作,影响系统性能。 StringBuffer是对象本身操作,而不是产生新的对象。因此在有大量拼接的情况下,建议使用StringBuffer。 String是线程不安全的,而StringBuffer是线程安全的。 需要注意是Java从JDK5开始,在编译期间进行了优化。如果是无变量的字符串拼接时,那么在编译期间值都已经确定了的话,javac工具会直接把它编译成一个字符常量。比如: String str = "关注微信公众号" + "“Java精选”" + ",面试经验、专业规划、技术文章等各类精品Java文章分享!"; 在编译期间会直接被编译成如下: String str = "关注微信公众号“Java精选”,面试经验、专业规划、技术文章等各类精品Java文章分享!"; Java 中 3*0.1 == 0.3 返回值是什么? 3*0.1==0.3返回值是false 这是由于在计算机中浮点数的表示是误差的。所以一般情况下不进行两个浮点数是否相同的比较。而是比较两个浮点数的差点绝对值,是否小于一个很小的正数。如果条件满足,就认为这两个浮点数是相同的。 System.out.println(3*0.1 == 0.3); System.out.println(3*0.1); System.out.println(4*0.1==0.4); System.out.println(4*0.1); 执行结果如下: false 0.30000000000000004 true 0.4 分析:3*0.1的结果是浮点型,值是0.30000000000000004,但是4*0.1结果值是0.4。这个是二进制浮点数算法的计算原因。 a=a+b 和 a+=b 有什么区别吗? +=操作符会进行隐式自动类型转换,a+=b隐式的将相加操作结果类型强制转换为持有结果的类型,而a=a+b则不会自动进行类型转换。 byte a = 127; byte b = 127; a = a + b; a += b; a = a + b; 编译报错: Type mismatch: cannot convert from int to byte Java 中线程阻塞都有哪些原因? 阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪),Java提供了大量方法来支持阻塞。 方法名 方法说明 sleep() sleep()方法允许指定以毫秒为单位的一段时间作为参数,它使得线程在指定的时间内进入阻塞状态,不能得到CPU时间,指定的时间一过,线程重新进入可执行状态。典型地,sleep()被用在等待某个资源就绪的情形:测试发现条件不满足后,让线程阻塞一段时间后重新测试,直到条件满足为止 suspend()和resume() 两个方法配套使用,suspend()使得线程进入阻塞状态,并且不会自动恢复,必须其对应的resume()被调用,才能使得线程重新进入可执行状态。典型地,suspend()和resume()被用在等待另一个线程产生的结果的情形:测试发现结果还没有产生后,让线程阻塞,另一个线程产生了结果后,调用resume()使其恢复。 yield() yield()使当前线程放弃当前已经分得的CPU时间,但不使当前线程阻塞,即线程仍处于可执行状态,随时可能再次分得CPU时间。调用yield()的效果等价于调度程序认为该线程已执行了足够的时间从而转到另一个线程 wait()和notify() 两个方法配套使用,wait()使得线程进入阻塞状态,它有两种形式,一种允许指定以毫秒为单位的一段时间作为参数,另一种没有参数,前者当对应的notify()被调用或者超出指定时间时线程重新进入可执行状态,后者则必须对应的notify()被调用。

优秀的个人博客,低调大师

2020报表软件产品整理

开源报表却越来越受到程序员热烈追捧,如ireport、 Jsper report、jfreechart这样的免费,开源的JAVA报表工具,在一下开源的JAVA报表相关的论坛里面都是热火朝天,发问者众多。发现都会有利有弊,近因为公司需求的原因,我上网查找了好多报表工具和试用体验,下面是我向大家推荐5个开源报表工具。 1.iReport iReport是为JasperReports设计的强大的,直观的,易于使用的可视化报表设计器,用Visual J++为Win32平台编写。iReport允许用户可视化地编辑XML JasperDesign文件,可以和其它数据库通过JDBC通信。在设计模板时可以以Html、Excel、Pdf等多种方式进行预览;用它生成的文件有.jrxml、.jasper两种文件,其中.jrxml就是我们设计时可视化编辑的xml文件,.jasper是经过编译.jrxml后生成的类文件,也就是我们最终在项目中用的报表模板文件。 2. JasperReport JasperReports是一个基于Java的开源报表工具,基于GPL开源许可协议, 完全采用java编写, 支持多种数据源,可打印或导出多种文件格式,支持PDF、HTML、XLS、CSV和XML文件输出格式。JasperReports是当前Java开发者最常用的报表工具。 JasperReport也包含多个组件: JasperReports Library 开源报表引擎库JasperReports Server 是一个独立可嵌入的报表服务器,对数据进行分析和提供定时任务服务。Jaspersoft Studio 是基于Eclipse的报表设计器,它能创建包含图表、图像、子报表、交叉表等复杂的报表。数据源可以是JavaBeans,XML,CSV Hibernate。Jaspersoft ETL 是易于部署和执行的开源ETL系统, 创建一个综合的数据仓库和数据集。 iReport是为JasperReports设计的强大的,直观的,易于使用的可视化报表设计器,采用纯Java开发。这个工具允许用户可视化编辑包含charts,图片,子报表等的复杂报表。**3、jfreechart** JFreeChart 是一款易于扩展的纯Java编写绘图图库, 使用它可以生成线图、柱状图、饼图、曲线图、面积图、甘特图、仪表盘、混合图等多种图表,并可将图表输出为PNG、JPEG图片。 稳定、轻量级,支持多种图表类型一个灵活的设计,很容易扩展,并应用于服务器端和客户端的应用程序支持多种输出类型,包括Swing组件和JavaFX组件、图像文件(包括PNG和JPEG)和矢量图形文件格式(包括PDF、EPS和SVG)JFreeChart是开源的,基于GNU通用公共许可证 (LGPL)开源许可协议 这里要强调的是:iReport-0.5.0中集成了jasperreports-0.6.7、jfreechart-0.9.21,所以用iRepot-0.5.0就完全可以开发报表了,但在iReport中进行图形统计报表的开发没有采用,而是直接引用其组件包进行自己手写类来开发的(因为对图形统计图的开发iReport支持的并不好) 4、Pentaho Pentaho将你所有的数据转变成有意义的信息,为您设计Pentaho报表,一套开源的工具,允许你创建完美的报表,支持输出Excel、PDF、HTML、文本、富文本文件,XML和CSV,生成的报表可以很容易地从各种来源提炼成可读的数据。 Pentaho报表的开发是通过创建一个灵活且简单易用的报表引擎。这是一套开源工具,包括Report Designer、Reporting Engine和Reporting SDK。 5、BIRT BIRT 是基于 Eclipse 的一款开源报表,创建数据的可视化和报表,可以嵌入到富客户和Web应用程序。 BIRT主要由两部分组成:一个用于创建BIRT设计的可视化报表设计器,以及可以部署到任何java环境运行时组件。BIRT项目还包括一个图表引擎,可集成到BIRT的一个应用程序图表。 BIRT设计模板保存为XML,可以访问多个不同的数据源包括JDO数据存储,jfire脚本对象、POJOs、SQL数据库、Web服务和XML。 报表方面目前测试做些简单的行式、分组交叉类报表还比较容易,稍微复杂点可能做起来有点麻烦(比如多数据集取数方面有点没搞懂,还未测试),这点实际和没有完善的学习文档有关,不方面查找。 统计图方面,常用的柱图、饼图、折线都提供,但其他的种类就相对少了,另外图形有点老,看着不够高大上,只能说有此功能了。 补充一点,本人是作为一个开发人员来测试birt,对eclipse熟悉,所以上手感觉还可以,但对于只是有sql基础、简单了解java或jsp开发的就来搞birt的话,还是会有很多问题。 Ps:这个就看领导们安排什么水平的员工来开发报表了。 以上是常用的 JAVA 开源报表开发软件,之所有选择它们是因为有专业的报表软件、纯JAVA的、有专业论坛提供大家资源分享、免费(这是最重要的)、用起来更为灵活。综上所述决定性因素只有应该:免费、灵活。 那么报表软件,到底免费与收费孰优孰劣? 首先,免费的java报表开发能给我们带来什么?答案显而易见:拥有了一定可用性的报表软件;开源的代码能够拥有灵活的可定制能力和完全的控制;最重要的是免费。但是开源工具都有个通病,就是开发和维护成本比较大,如果你企业想上报表工具,怎么也得招/培养2、3个开发人,而且学习资料多为英文。而且Jsper report、ireport的所有帮助文档是收费的,文档倒是相当细致,需要花大量的时间阅读。这才明白:所谓开源不可能真的有人那么无聊为人民服务,说白了还是要挣钱的,否则产品的后续研发怎么办? 而且Jsper report、ireport的制表能力实在一般,老外的东西,本质上就不符合咱的报表习惯,报表似乎就该这么做,做不出来的报表似乎就应该写程序、写代码,最后用工具的结果还是去写代码,还不如不用呢。 另外,在论坛里,像“请教高手某某问题如何解决”这类的帖子比比皆是。首先解决问题的时效性不高,需要等待不能即时被答复,而且也不一定有人能遇到过同样的问题,并愿意热心帮你解决,这时就会干着急,希望有人技术支持一下。其实仔细算算,花在这些问题上的时间成本、人工成本,还不如买一个收费的工具? 选择收费的报表软件 首先在你有问题的时候能找专业的java报表厂商支持你,不用在论坛里发些的不一定有准确答复的求问贴了,更有甚者可以叫厂商直接帮你做部分表。做事是讲究效率的,与其浪费时间去自学开源工具的文档,还不如边学边做不懂就直接问报表工具厂商。这样能更顺利高效的完成项目。 我以前的公司做项目时老板就是不肯花钱,以为是省钱,结果,由于不能及时完成折了好几个项目。所以,再碰上选择报表软件的时候,一定不要怕跟老板倾诉: 报表制作其实是很专业的的活,花钱买一个工具比用开源工具划算。 下面的商业报表工具,其实成本算下来也无差,产品提供部署服务,可以走项目,后期还有技术维护。 商用报表工具: 1、XJR快速开发平台报表工具 使用多年钻坚研微的成熟稳定的第三方插件,提供大量标准报表模板,满足各行业不同的需要。使用简单,会数据库就可以用。不需要再出报表授权费用。还有很重要的一点就是已经集成在开发平台内不需要再做整合。XJR快速开发平台融合了时下最实用的web及互联网应用技术,集成多类业务场景。面向服务/接口设计,可轻松集成或集成到外部系统,轻松整合企业现有资源。 价格平民,功能实用,符合中国国情的报表需求; 开发上手快,不需要专业人员,会数据库就可以用。 2、帆软报表 功能较为齐全,适应大多数行业的功能, 价格较贵,基础版价格:80000元/ 服务器,高级版价格:500000/服务器 3、 水晶报表CrystalReport SAP公司的商业报表工具,作为SAP“集团”下的报表组件模块。10年事前盛行一时,后被SAP收购。但水晶报表(Crystal Report)在理论上只支持单数据集,对多集的支持依赖于数据库的运算能力(叉乘与联合等或写存储过程),多库一般难以支持。 4、 cognos IBM Cognos Business Intelligence Cognos功能是很强大的,但是操作的复杂度比较高。如果是普通的列表,操作确实也简单,如果是格式复杂或者是统计数据来源表比较多的时候,至少对于我这样的新手来说,操作起来就没那么得心应手。 5、其他:例如Oracle的BIEE算是商用BI工具,包括在商业智能方案里。国产的还有润乾、斯迈特等等,不多赘述。 一句话总结: 商用>开源,开源需要很强的代能力,项目上线时间长,容易不稳定,后续维护需要不停改代码。 国产>国外,这一点上不要“崇洋媚外”了,事实证明,国产报表更懂表格表姐的心。 不管选择哪类工具,最重要的还是满足业务需求,在满足的前提下寻找合适的工具。

优秀的个人博客,低调大师

整理大型网站架构必知必会的几个服务器知识

最近看书及系统开发部署过程中的一些心得,再对照自己之前的从业经验,很多都是听闻而已,当然也有一些已经很熟悉,有的正在搞,有的未来希望可以着手付诸实施,留此存照。 1、负载均衡服务器 负载均衡服务器主要作用是实现某些类型服务器的规模扩展。比如对于系统前端的web服务器和后端的数据库服务器,想通过加服务器实现N+1横向扩展,通过多台服务器负载分担压力,负载均衡必不可少。 2、web服务器 最常见,内存要求不是很高但cpu要求较高,主要用于部署各种web应用,如带界面的web页面、不带界面的web服务、wcf等等。 3、缓存服务器 大中型网站,分布式缓存已是标配,缓存服务器专门用于部署分布式缓存,一般而言对内存和带宽要求较高。 4、消息队列服务器 队列是系统解耦利器,也是大中型分布式系统标配,没有队列,业务系统很容易高度耦合,系统吞吐量也会很快遭遇瓶颈。 5、文件服务器 分布式文件系统,专门用于存储业务系统需要的各种文件如图片、多媒体文件等。 6、索引服务器 用于网站全文索引,搜索必备。对内存和CPU要求较高,大型网站,通常还需要支持主从备份和容错,甚至多实例索引集群。 7、搜索服务器 通常需要部署多台,否则查询多了性能撑不住,对内存要求不高。有的中小型站点,索引和搜索服务器在物理和逻辑上都是同一台服务器。 8、作业服务器 主要用于后端应用程序大批量大数据量复杂业务逻辑的定时作业,大多数互联网公司标配,某些企业的定时调度框架是直接部署在web服务器上的,可以减少这里的所谓作业服务器。 9、数据库服务器 主要用于存储和查询数据。数据库已是各种系统实际上的标配,内存和CPU都要求极高,网络和硬件要求也不低。大中型网站还需要支持数据库的主从备份和容错,甚至多实例的数据库集群。 通常,大中型的互联网应用会经历一个从单一的数据库服务器,到Master/Slave主从服务器,再到垂直分区(分库),然后再到水平分区(分表,sharding)的过程。而在这个过程中,Master/Slave以及分库相对比较容易,对应用的影响也不是很大,但是分表会引起一些棘手的问题,比如不能跨越多个分区join查询数据,如何实现DB负载等等,这个时候就需要一个通用的DAL框架来屏蔽底层数据存储对业务逻辑的影响,使得底层数据的访问对应用完全透明化。 10、nosql服务器 海量数据处理的兴起,各种nosql产品层出不穷,nosql服务器主要用于处理海量数据,支持存储、查询、分片等。 web应用中,有两个一直是不好实现横向扩展或者由于历史遗留问题实现代价非常大的东西,如你所知,就是:A、数据库 B、网络带宽。 而某些nosql的出现很可能解决这个历史遗留难题,现在已经有nosql产品弥补了关系型数据库天生不支持横向扩展的缺点,在特定场景下正在替代关系型数据库。 11、其他 需求不断变化和应用需要,某些互联网企业还可能衍生出基于安全的授权/证书服务器,全局唯一的流水号服务器,会话服务器等等。 参考: <<大型网站技术架构>> <<构建高性能web站点>> http://www.cnblogs.com/terryli/archive/2008/04/06/1139121.html http://www.cnblogs.com/ejiyuan/archive/2010/10/29/1796292.html http://kb.cnblogs.com/page/99549/ http://highscalability.com/blog/2014/7/21/stackoverflow-update-560m-pageviews-a-month-25-servers-and-i.html http://www.infoq.com/articles/perera-data-storage-haystack http://lethain.com/introduction-to-architecting-systems-for-scale/ 本文转自JeffWong博客园博客,原文链接:http://www.cnblogs.com/jeffwongishandsome/p/some-knowledge-about-server.html,如需转载请自行联系原作者

优秀的个人博客,低调大师

整理了一些简单好用的开源文件传输工具

这里有一些开源的文件传输工具,简单好用,值得好评! FileZilla Client - FTP 文件传输工具 FileZilla 是一种快速、可信赖的 FTP 客户端以及服务器端开放源代码程式,具有多种特色、直观的接口。 FileZilla 不仅支持 FTP,还支持 FTP over TLS (FTPS) 和 SFTP 。 了解更多:https://www.oschina.net/p/filezilla+client 百灵快传(B0Pass) - 超大文件传输工具 百灵快传(B0Pass)是一款基于 Go 语言的高性能 “手机电脑超大文件传输神器”、“局域网共享文件服务器”。 主要使用场景: (手机电脑共享文件)电脑上双击执行 --> 手机扫码 --> 手机上的大文件传到电脑、或者电脑传文件到手机 了解更多:https://www.oschina.net/p/b0pass LFTP 命令行 FTP 工具 LFTP 是一款非常著名的字符界面的文件传输工具。支持 FTP、HTTP、FISH、SFTP、HTTPS 和 FTPS 协议。 如果还需要 ssl 的支持,则需要额外的 OpenSSL 依赖。 了解更多:https://www.oschina.net/p/lftp trzsz.js - 文件传输工具 trzsz (trz /tsz) 是一个兼容 tmux 的文件传输工具,和 lrzsz ( rz / sz ) 类似,并且有进度条,支持目录传输,支持拖动上传。 了解更多:https://www.oschina.net/p/trzsz-js qrcp - 扫码传输文件的工具 qrcp 是一个文件传输工具,扫描终端上的二维码,可以通过 Wi-Fi 直接在 PC 与移动设备上传输文件,支持发送与接收。 了解更多:https://www.oschina.net/p/qrcp NitroShare - 局域网文件传输工具 NitroShare 是一个局域网文件传输工具,支持 Windows、OS X 和 Linux,基于 Qt 开发。 特点:支持跨平台、自动发现局域网的设备、简单直观的用户界面、可传输整个目录 了解更多:https://www.oschina.net/p/nitroshare FileCodeBox-文件快递柜 FileCodeBox 是一个轻量级文件快递柜,用户以匿名口令分享文本、文件,像拿快递一样取文件。 了解更多:https://www.oschina.net/p/filecodebox Bigfile-go- 文件传输管理系统 Bigfile 是使用 Golang 开发的一个文件传输管理系统,支持通过 HTTP API,RPC 调用以及 FTP 客户端管理文件。 了解更多:https://www.oschina.net/p/bigfile-go OnionShare安全可靠的匿名文件分享工具 OnionShare 是一个开源工具,可以让你安全和匿名地共享任何大小的文件。 它通过启动一个 Web 服务器工作,作为 Tor onion 服务被访问,并生成一个临时的 URL 以访问和下载文件。将文件托管到自己的电脑,并使用 Tor onion 服务,使其可以通过互联网临时访问。其他用户只需要使用 Tor 浏览器从你的主机下载文件即可。 了解更多:https://www.oschina.net/p/onion-share croc-跨平台文件传输工具 croc 是一种允许任意两台计算机简单安全地传输文件和文件夹的工具。 它提供端到端加密、允许多个文件传输、允许恢复被中断的传输,同时不需要本地服务器或端口转发。 了解更多:https://www.oschina.net/p/croc tl-rtc-fileweb 端文件传输 tl-rtc-file 使用 webrtc 在 web 端传输文件,支持传输超大文件,P2P 点对点传输。 优点 :分片传输,跨终端,不限平台,方便使用,内网不限速,支持私有部署,代码简单,通俗易懂,可二次开发,可作入门学习。 了解更多:https://www.oschina.net/p/tl-rtc-file 本文所述开源文件传输工具均已收录至 Awesome 软件系列之《开源文件传输工具》。如有其他高质量文件传输工具,欢迎大家在评论区补充留言~我们会及时更新到本文和软件专栏中~

资源下载

更多资源
优质分享App

优质分享App

近一个月的开发和优化,本站点的第一个app全新上线。该app采用极致压缩,本体才4.36MB。系统里面做了大量数据访问、缓存优化。方便用户在手机上查看文章。后续会推出HarmonyOS的适配版本。

Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。