0%

从 iPhone 诞生至今,智能手机风靡全球已将近 20 年,智能手机操作系统 iOS 和 Android 也成为当仁不让的顶流般的存在,而作为其背后的灵魂,移动应用也随着技术的发展已经越来越丰富。如果从技术层面来讲,移动 App 也从最开始单一的原生开发(Native App)模式,演变出了混合开发(Hybird App)、网页应用开发(Web App),为什么会有这种发展的变化呢?

因为原有的 Native App 有一个明显的痛点,就是相同的功能需要在不同的平台上都实现一遍,所以就有了一个很迫切的需求,能否只需要写一次代码,就可以在各个端都运行?

移动跨平台的逻辑

跨平台开发从本质上讲是为了增加业务代码的复用率,减少因为要适配多个平台带来的工作量,从而降低开发成本。在提高业务专注度的同时,能够为用户提供一致的用户体验,实现“多快好省”的效果。

跨平台是跨哪些平台?怎么样的跨平台逻辑?从当前的实际情况来看,移动端跨平台需求主要集中在以下 3 点:

  • 桌面端跨移动端:桌面向移动端过渡的早期,希望 PC Web 与移动 Web 复用同一套代码。
  • Native 跨 Web:一套功能差不多的 Web 页能够在端外访问,需要跨 Native App 与 Web。
  • 跨系统双端:出于开发效率等原因,希望 Android、iOS 双端复用一套业务代码,这也是目前主要的需求点。

而放眼未来,我们预见可能还会有这些跨平台需求:

  • 跨小程序/轻应用:即用即走的轻量级应用,如各平台的小程序、 Android 快应用、iOS App Clips。
  • 跨 IoT 设备:各种有显示屏的设备都会成为新的入口,如车载设备、智能电视等。

移动跨平台方案的发展

不仅是移动应用的开发模式在持续的演变,跨平台开发方案也紧紧的跟随着开发模式的变化持续的演进,按照技术的发展,跨平台方案可以分为三个时代。

1、Web 容器时代

基于 Web 相关技术通过浏览器组件来实现界面及功能,典型的框架包括 Cordova、Ionic 和微信小程序。Web 时代的方案,主要采用的是原生应用内嵌浏览器控件 WebView 的方式进行 HTML5 页面渲染,并定义 HTML5 与原生代码交互协议,将部分原生系统能力暴露给 HTML5,从而扩展 HTML5 的边界。这类交互协议,就是我们通常说的 JS Bridge。

img

2、泛 Web 容器时代

采用类 Web 标准进行开发,但在运行时把绘制和渲染交由原生系统接管的技术,代表框架有 React Native、Weex 和快应用等。过渡到泛 Web 容器时代,优化了 Web 容器时代的加载、解析和渲染这三大过程,把影响它们独立运行的 Web 标准进行了裁剪,以相对简单的方式支持了构建移动端页面必要的 Web 标准(如 Flexbox 等),也保证了便捷的前端开发体验;同时,这个时代的解决方案基本上完全放弃了浏览器控件渲染,而是采用原生自带的 UI 组件实现代替了核心的渲染引擎,仅保持必要的基本控件渲染能力,从而使得渲染过程更加简化,也保证了良好的渲染性能。

img

3、自绘引擎时代

自带渲染引擎,客户端仅提供一块画布即可获得从业务逻辑到功能呈现的多端高度一致的渲染体验。Flutter,是为数不多的代表。Flutter 开辟了一种全新的思路,即从头到尾重写一套跨平台的 UI 框架,包括渲染逻辑,甚至是开发语言。

img

移动跨平台技术方案的对比

对比现有的跨平台技术和解决方案也可以分为三类,分别是 Web 跨端、容器跨端、小程序跨端。

1、Web 跨端

Web 跨端比较好理解,因为 Web 与生俱来就有跨端的能力,因为只要有浏览器或 WebView,现在绝大多数端上(甚至包括封闭的小程序生态)都支持 Webview,所以只要开发网页然后投放到多个端即可轻松跨平台,例如 Web App、PWA(Progressive Web Apps)、Hybrid App、PHA(Progress Hybrid App)。

优点:

  • 没有额外的学习成本,一套基础技术吃天下
  • 不依赖特殊的配套设施,从开发、调试到运维等所有工程化环节都是通用的
  • 背靠 npm 庞大的生态,百万模块,应有尽有

缺点:

  • 经常会遇到白屏、卡顿等情况,用户的体验不佳
  • 无法调用系统的权限,例如多媒体、蓝牙、相机等
  • 性能不好,对内存的消耗大

2、容器跨端

另一种统一多端的思路是将 Native 定制成标准容器,让同一份代码跑在一个个标准容器中。比较典型的代表是 React Native 、Flutter、Weex,这类方案通过尽可能的取长补短,综合了 Web 生态和 Native 组件,让 JS 执行代码后用 Native 的组件进行渲染,以解决抛弃 Web 历史包袱的问题。

具体来讲 React Native 可以跨 Android、iOS、Web、Windows 四端,Flutter 可以跨 Android、iOS、Web、Linux 四端,Weex 可以跨 Android、iOS、Web 三端。

优点:

  • Flutter 快速的开发,富有表现力的精美 UI 和类似本机的性能
  • React Native 专注于用户界面,使应用程序开发人员能够构建高度可靠的界面
  • Weex 页面就像开发普通网页一样;在渲染 Weex 页面时和渲染原生页面一样

缺点:

  • React Native 没有提供的需要自定义的应用,仍然需要使用原生开发
  • Flutter 构建的应用程序文件很大,没有广泛的资源基础,这意味着可能找不到开发所需的第三方库和包
  • Weex 由于起步比较晚,社区活跃度不如 RN,资料和开源项目也相对较少

3、小程序跨端

小程序跨端也比较好理解,就是让同样代码的小程序能够运行在多个 App 中,例如开发完一个小程序除了让其运行在微信之外,还能运行在支付宝、百度等超级 App,甚至是自己的 App 中。

如果说小程序仍然是依靠 Web 技术运行的,那为什么还要单独去使用小程序呢?就像前面所说到的一样,Web 始终没法调用例如相机、蓝牙等这样的权限,并且用户使用体验会收到一定的影响。而小程序则不同,小程序具有强大的 Web 渲染引擎、提供丰富组件、支持本地缓存、避免 DOM 泄露等,并且其初衷是开放,例如微信、支付宝这样的超级 App 也都相继开放了小程序上架能力,小程序逐渐成为跨 App 的正规方式。

后期也甚至出现了例如 FinClip 这样的小程序容器,可以让个人或企业自己的 App 具备小程序的运行能力,让其他 App 能够具有超级 App 一致的小程序跨端能力。

优势:

  • 具备类似 Native App 的体验度,使用较为流畅丝滑
  • 可以获取用户的相册、多媒体、蓝牙等基础权限
  • 可以通过便捷化的上下架方式完成相关页面和业务的热更新

缺点:

  • 大平台的框架标准不统一,会稍微有影响,但都大同小异,W3C 也在做小程序的标准化工作
  • 部分的插件会用到原生相关的技术

redis的常见面试问题

什么是缓存击穿

一个访问量比较大的key,缓存过期,导致所有请求直接打到DB上。

解决方案

1. 加锁更新: 就是写缓存逻辑加排它锁,只有一个线程去访问db获取缓存内容写缓存。 
1. 将过期时间组合写到value中,通过异步方式不断更新缓存内容刷新过期时间,防止此类现象。 (线上一般很少使用这种方式) 

什么是缓存穿透

缓存穿透是指:查询缓存和数据库中都不存在的数据。 这样每次请求都直接打到数据库。 就好像缓存不存在

导致问题: 不存在的数据每次请求都要到存储层查询,失去了缓存保护后端存储的意义。

原因:

  1. 业务代码有问题

     2. 恶意攻击,爬虫造成空命中。
    

解决方案:

  1. 缓存空值/默认值

    1. 这种方式是在数据库不命中情况把一个空值,或者默认值保存到缓存,之后再访问这个数据就会从缓存中获取,保护了数据库

    缓存空值会造成的问题:

    1. 空值被缓存,意味这缓存层需要更多的内存空间,有效解决办法: 这类缓存设置一个较短的过期时间,让其自行删除。 
    2. 缓存层和db层数据会有一段时间的不一致,可能对业务有一定影响。 解决方案: 利用消息队列或者其他异步方式清理缓存中的空对象。 
    
  2. 布隆过滤器方案:

    1. 可以在存储和缓存之前加一个布隆过滤器,做一层过滤。 布隆过滤器会保存数据是否存在,如果判断是不不存在直接返回,就不会访问存储

缓存雪崩

在某一时刻 发生大规模的缓存失效情况。 例如缓存服务宕机,大量key在同一时间过期。 这样的后果就是大量请求进来直接打到db上,可能导致整个系统的崩溃。 称为雪崩。

缓存雪崩是三大缓存问题中最严重的

预防和处理:

  • 提高缓存可用性
    1. 集群部署:通过集群提升缓存的可用性
    2. 多级缓存:设置多级缓存。 第一级缓存失效的情况下访问第二级缓存,每一级缓存失效时间都不同。
  • 过期时间调整
    1. 均匀过期: 为了避免大量缓存同一时间过期,可以把不同key的过期时间随机生成,避免过期时间太过集中。
    2. 热点数据永不过期。
  • 熔断降级
    1. 服务熔断: 当缓存服务宕机或者响应超时,为了防止整个系统出现雪崩,暂时停止业务服务访问缓存系统。
    2. 服务降级: 当出现大量缓存失效,而且处于高并发高负荷的情况下,在业务逻辑内部舍弃一些非核心接口和数据的请求。 直接返回一个提前准备好的 fallback(退路) 错误处理信息

问题:如何保证本地缓存和redis缓存数据的一致性

这个问题日常工作中应当比较常遇到,通用解决方案:

方案一:

​ 业务逻辑确定本地缓存有效期。就是给本地缓存添加一个相对短的过期时间。 适用与一些非重点数据,例如用户头像等可以接受短时间数据不一致情况。

方案二:

采用Redis的发布/订阅机制(或者Rocketmq等) 分布式集群的所有阶段订阅删除。

方案三:

通过调用接口方式删除。 实际操作是在数据变更发生是,触发接口调用所有容器删除本地缓存接口。 这样做的情况 需要在每个容器启动时注册当前容器ip和端口信息。 方案优点: 响应快,稳定性好。 缺点: 实现复杂度高。

方案四:

通过Zookeeper中间件方式: 实现单个容器操作数据变更时 其他容器接收到数据变更消息删除本地缓存重新拉取。

问题: 如何保证redis缓存和mysql数据的一致性

网上资料的解决方案:

选择合适的缓存更新策略

  1. 删除缓存而不是更新缓存

    当一个线程对缓存的key进行写操作的时候,如果其他线程进来读数据库的时候,读到的就是脏数据。 产生了数据不一致问题。

    相比较而言: 删除缓存的速度,比更新缓存的速度快很多。 所以用时相对小很多。 读脏数据的概率也小很多。

  2. 先更新数据后删除缓存

    更新数据耗时是在删除缓存的百倍以上 。 如果先删缓存,在数据更新成功之前,其他线程获取数据会把脏数据写入到缓存。

    目前最流行的缓存读写策略就是采用先更新数据库,再删缓存的方式。

缓存不一致的处理

如果不是并发特别高,对缓存依赖性很强,其实一定程度的不一致是可以接受的。

缓存和数据库数据不一致常见的原因:

  • 缓存key删除失败
  • 并发导致写入脏数据

解决方案:

通过消息队列保证key被删除。 把要删除或者删除失败的key写入消息队列,利用消息队列的重试机制,重试删除对应key。

缺点: 代码侵入

数据库订阅+消息队列保证key被删除 可以用一个服务监听数据库的binlog 获取需要操作的数据。 然后用一个公共服务获取订阅程序传来的信息,进行缓存删除操作。 这种方式降低了对业务代码侵入,但是复杂度提升了。 适合基建完善的大厂。

延时双删防止脏数据 在缓存不存在的情况,写入了脏数据,这种情况先删缓存,再更新数据库的缓存更新策略下发生比较多。 解决方案: 延时双删

操作: 在第一次删除缓存后过了一段时间再次删除缓存。 这种方式的延时时间需要仔细斟酌。

设置缓存过期时间兜底

这个是朴素但是有用的方案,给缓存设置一个过期时间,即使发生数据不一致问题,它也不会永远不一致下去。

**下面是我自己整理的解决方案: **

这个问题遇到的应当是最多的。

常规解决方案:

方案一:

先删除缓存数据,然后执行mysql的数据更新。 这种方案 如果缓存没有有效期的情况 可能存在脏读。 就是缓存了 更新前的数据。

解决方案:mysql 数据更新后 再次删除缓存。 也就是二次删除。 这样可以保证redis的数据 跟db数据一致。 这里有个前提,redis数据读取的需要是主库数据,如果是从库数据,可能存在主从同步延迟,导致重新读取的数据还是旧数据情况。

方案二:

异步删除缓存。 先更新db数据,然后通过binlog或者rocketmq等方式 消费消息时进行缓存key的删除。 这样可以保证缓存数据一定被成功删除。 例如rocketmq消息消费失败,就是删除失败情况 可以多次尝试。 缺点: 对业务侵入大。

方案三:

设置缓存过期时间,其实这个应当是基本要求,所有缓存都要设置过期时间。 但是设置过期时间后会引发另外一个 redis穿透问题。 需要对这种情况进行特殊处理。

大纲

mybatis代码结构分为三个大部分

  • 接口层 提供SqlSession
  • 核心层
  • 基础支持层 包含: 解析器模块,反射器模块,日志模块,类型转换模块 。。。 (待完善)

基础支持层

解析器模块:

这个模块主要是用于解析Xml文件,有mybatis-config.xml 和 Mapper.xml 等。 

包含多个重点类: Xnode 是一个包装Dom的node类,用于解析属性信息。 使用到了 XpathParse 类 解析路径,这个parse使用了通用占位符解析,可以对属性值的占位符进行替换。 支持默认值。

反射器模块

这个模块是mybatis增强后的反射模式,提供Reflector 每个对象对应一个类。 对这个类进行属性解析,方法,构造方法等的记录,能够支持操纵类。 

类型转换模块

这个模块主要作用是用于对 JavaType和JdbcType进行互相转换。 由于mysql的字段类型和Java的字段类型不一致,所以才有的这个模块。 这里使用TypeHandler 接口定义了 设置属性和获取属性的方法。 有一个BaseTypeHandler 抽象实现类,实现了空值的设置和获取方法,非空置的处理 交给对应子类实现。 

类型转换模块的使用是通过TypeHandlerRegiester 进行注册管理。 mybatis默认实现了java所有常用类型对应TypeHandler的实现方法。 可以支持自定义类型实现方法。 通过在mybatis-config.xml 文件添加 typeHandler 配置指定实现类。 

类型转换模块提供了 类型别名功能,对表名,列名等支持设置别名。 另外也支持对类设置别名。 mybatis对常用类型默认直接设置了别名。 

日志模块

这个模块是对常用第三方日志组件的封装,通过适配器模式定义了 Log接口。 为每一个Log组件实现了具体方法。 
 

Java Spring 常用注解

Web:

  • @Controller:组合注解(组合了@Component 注解),应用在 MVC 层(控制层)。
  • @RestController:该注解为一个组合注解,相当于@Controller 和@ResponseBody 的组合,注解在类上,意味着,该 Controller 的所有方法都默认加上了@ResponseBody。
  • @RequestMapping:用于映射 Web 请求,包括访问路径和参数。如果是 Restful 风格接口,还可以根据请求类型使用不同的注解:
    • @GetMapping
    • @PostMapping
    • @PutMapping
    • @DeleteMapping
  • @ResponseBody:支持将返回值放在 response 内,而不是一个页面,通常用于返回 json 数据。
  • @RequestBody:允许 request 的参数在 request 体中,而不是在直接连接在地址后面。
  • @PathVariable:用于接收路径参数,比如 @RequestMapping(“/hello/{name}”)申明的路径,将注解放在参数中前,即可获取该值,通常作为 Restful 的接口实现方法。

容器:

  • @Component:表示一个带注释的类是一个“组件”,成为 Spring 管理的 Bean。当使用基于注解的配置和类路径扫描时,这些类被视为自动检测的候选对象。同时@Component 还是一个元注解
  • @Service:组合注解(组合了@Component 注解),应用在 service 层(业务逻辑层)。
  • @Repository:组合注解(组合了@Component 注解),应用在 dao 层(数据访问层)。
  • @Autowired:Spring 提供的工具(由 Spring 的依赖注入工具(BeanPostProcessor、BeanFactoryPostProcessor)自动注入)。
  • @Qualifier:该注解通常跟 @Autowired 一起使用,当想对注入的过程做更多的控制,@Qualifier 可帮助配置,比如两个以上相同类型的 Bean 时 Spring 无法抉择,用到此注解
  • @Configuration:声明当前类是一个配置类(相当于一个 Spring 配置的 xml 文件)
  • @Value:可用在字段,构造器参数跟方法参数,指定一个默认值,支持 #{} 跟 \${} 两个方式。一般将 SpringbBoot 中的 application.properties 配置的属性值赋值给变量。
  • @Bean:注解在方法上,声明当前方法的返回值为一个 Bean。返回的 Bean 对应的类中可以定义 init()方法和 destroy()方法,然后在@Bean(initMethod=”init”,destroyMethod=”destroy”)定义,在构造之后执行 init,在销毁之前执行 destroy。
  • @Scope:定义我们采用什么模式去创建 Bean(方法上,得有@Bean) 其设置类型包括:Singleton 、Prototype、Request 、 Session、GlobalSession。

AOP:

  • @Aspect:声明一个切面(类上) 使用@After、@Before、@Around 定义建言(advice),可直接将拦截规则(切点)作为参数。

    • @After :在方法执行之后执行(方法上)。
    • @Before: 在方法执行之前执行(方法上)。
    • @Around: 在方法执行之前与之后执行(方法上)。
    • @PointCut: 声明切点 在 java 配置类中使用@EnableAspectJAutoProxy 注解开启 Spring 对 AspectJ 代理的支持(类上)。

事务:

  • @Transactional:在要开启事务的方法上使用@Transactional 注解,即可声明式开启事务。

总结通过文件获取内容方式

在java中有很多读取文本文件的方法。文本文件由字符组成,因此可以使用Reader类。在java中读取文本文件也有一些实用程序类。

  • 使用Files类读取文本文件;
  • 使用FileReader类读取文本文件;
  • 使用BufferedReader类读取文本文件;
  • 使用Scanner类读取文本文件;

方法一 使用java.nio.file.Files读取文本文件

使用Files类将文件的所有内容读入字节数组。Files类还有一个方法可以读取所有行到字符串列表。Files类是在Java 7中引入的,如果想加载所有文件内容,使用这个类是比较适合的。只有在处理小文件并且需要加载所有文件内容到内存中时才应使用此方法。

1
2
3
4
5
String fileName = "D:/maxsu/docs/source.txt";
Path path = Paths.get(fileName);
byte[] bytes = Files.readAllBytes(path);
List<String> allLines = Files.readAllLines(path, StandardCharsets.UTF_8);

方法二:使用java.io.FileReader类

可以使用FileReader获取BufferedReader,然后逐行读取文件。FileReader不支持编码并使用系统默认编码,因此它不是一种java中读取文本文件的非常有效的方法。

1
2
3
4
5
6
7
8
9
10
11
12


String fileName = "D:/maxsu/docs/source.txt";
File file = new File(fileName);
FileReader fr = new FileReader(file);
BufferedReader br = new BufferedReader(fr);
String line;
while((line = br.readLine()) != null){
// 一行一行地处理...
System.out.println(line);
}

方法三:使用java.io.BufferedReader

如果想逐行读取文件并对它们进行处理,那么BufferedReader是非常合适的。它适用于处理大文件,也支持编码。

BufferedReader是同步的,因此可以安全地从多个线程完成对BufferedReader的读取操作。BufferedReader的默认缓冲区大小为:8KB

1
2
3
4
5
6
7
8
9
10
11
12
13
String fileName = "D:/maxsu/docs/source.txt";
File file = new File(fileName);
FileInputStream fis = new FileInputStream(file);
InputStreamReader isr = new InputStreamReader(fis, cs);
BufferedReader br = new BufferedReader(isr);

String line;
while((line = br.readLine()) != null){
//process the line
System.out.println(line);
}
br.close();

方法四:使用Scanner读取文本文件

如果要逐行读取文件或基于某些java正则表达式读取文件,则可使用Scanner类。

Scanner类使用分隔符模式将其输入分解为标记,分隔符模式默认匹配空格。然后可以使用各种下一种方法将得到的标记转换成不同类型的值。Scanner类不同步,因此不是线程安全的。

1
2
3
4
5
6
7
8
9
10
11
Path path = Paths.get(fileName);
Scanner scanner = new Scanner(path);
System.out.println("Read text file using Scanner");
// 一行一行地读取
while(scanner.hasNextLine()){
//process each line
String line = scanner.nextLine();
System.out.println(line);
}
scanner.close();

方法五: 使用RandomAccessFile 读取文件

1
2
3
4
5
6
7
RandomAccessFile file = new RandomAccessFile("D:/maxsu/docs/readme.txt", "r");
String str;

while ((str = file.readLine()) != null) {
System.out.println(str);
}
file.close();

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
package com.journaldev.files;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Scanner;

public class JavaReadFile {

public static void main(String[] args) throws IOException {
String fileName = "D:/maxsu/docs/source.txt";

//使用Java 7中的Files类处理小文件,获取完整的文件数据
readUsingFiles(fileName);

//使用Scanner类来处理大文件,逐行读取
readUsingScanner(fileName);

//使用BufferedReader读取,逐行读取
readUsingBufferedReader(fileName);
readUsingBufferedReaderJava7(fileName, StandardCharsets.UTF_8);
readUsingBufferedReader(fileName, StandardCharsets.UTF_8);

//使用FileReader读取,没有编码支持,效率不高
readUsingFileReader(fileName);
}

private static void readUsingFileReader(String fileName) throws IOException {
File file = new File(fileName);
FileReader fr = new FileReader(file);
BufferedReader br = new BufferedReader(fr);
String line;
System.out.println("Reading text file using FileReader");
while((line = br.readLine()) != null){
//逐行读取
System.out.println(line);
}
br.close();
fr.close();

}

private static void readUsingBufferedReader(String fileName, Charset cs) throws IOException {
File file = new File(fileName);
FileInputStream fis = new FileInputStream(file);
InputStreamReader isr = new InputStreamReader(fis, cs);
BufferedReader br = new BufferedReader(isr);
String line;
System.out.println("Read text file using InputStreamReader");
while((line = br.readLine()) != null){
//逐行读取
System.out.println(line);
}
br.close();

}

private static void readUsingBufferedReaderJava7(String fileName, Charset cs) throws IOException {
Path path = Paths.get(fileName);
BufferedReader br = Files.newBufferedReader(path, cs);
String line;
System.out.println("Read text file using BufferedReader Java 7 improvement");
while((line = br.readLine()) != null){
//逐行读取
System.out.println(line);
}
br.close();
}

private static void readUsingBufferedReader(String fileName) throws IOException {
File file = new File(fileName);
FileReader fr = new FileReader(file);
BufferedReader br = new BufferedReader(fr);
String line;
System.out.println("Read text file using BufferedReader");
while((line = br.readLine()) != null){
//逐行读取
System.out.println(line);
}
//close resources
br.close();
fr.close();
}

private static void readUsingScanner(String fileName) throws IOException {
Path path = Paths.get(fileName);
Scanner scanner = new Scanner(path);
System.out.println("Read text file using Scanner");
//逐行读取
while(scanner.hasNextLine()){
//逐行处理
String line = scanner.nextLine();
System.out.println(line);
}
scanner.close();
}

private static void readUsingFiles(String fileName) throws IOException {
Path path = Paths.get(fileName);
//将文件读取到字节数组
byte[] bytes = Files.readAllBytes(path);
System.out.println("Read text file using Files class");
//read file to String list
@SuppressWarnings("unused")
List<String> allLines = Files.readAllLines(path, StandardCharsets.UTF_8);
System.out.println(new String(bytes));
}

}


背景

今天面试时遇到了一套计算题,计算逻辑实现很简单,被从控制台获取输入信息难住了。

所以面试结束后,查了下资料。 原来很简单……

从控制台获取输入内容的方式有那些

使用阅读缓冲器类

这个是经典的取输入方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) {
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
// Reading data using readLine
String name = null;
try {
while (!"0".equals(name)) {
name = reader.readLine();
System.out.println(name);
}

} catch (IOException e) {
throw new RuntimeException(e);
}


}

使用Scanner类

这可能是接受输入的最首选方法。Scanner 类的主要目的是使用正则表达式解析原始类型和字符串,但是,它也可用于在命令行中读取用户的输入

从标记化输入中解析基元(nextInt()、nextFloat()、…)的便捷方法。

正则表达式可用于查找标记。

阅读方式不同步

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
String s = in.nextLine();
System.out.println("You entered string " + s);
int a = in.nextInt();
System.out.println("You entered integer " + a);
float b = in.nextFloat();
System.out.println("You entered float " + b);
}

使用控制台类

它已成为从命令行读取用户输入的首选方式。此外,它可以用于读取类似密码的输入,而不用回显用户输入的字符;也可以使用格式字符串语法(如 System.out.printf())。

优点:

读取密码而不回显输入的字符。

读取方法是同步的。

可以使用格式字符串语法。

**不适用于非交互环境(例如 IDE)**。

1
2
3
4
public static void main(String[] args) {
String name = System.console().readLine();
System.out.println("You entered string " + name);
}

执行提示:

1
2
3
Exception in thread "main" java.lang.NullPointerException
at com.lin.springlearn.service.PrimeNumber.main(Test.java:37)

使用命令行参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args)
{
// check if length of args array is
// greater than 0
if (args.length > 0) {
System.out.println(
"The command line arguments are:");
// iterating the args array and printing
// the command line arguments
for (String val : args)
System.out.println(val);
}
else
System.out.println("No command line "
+ "arguments found.");
}

面试过程:

  1. 做题 7道

    1. string reverse 字符串反转
    2. 从文件中读取最大长度word
    3. sql查询 去重取id最小记录
    4. 验证ip
    5. 检测string中的重复字符
    6. 通过命令行输入表达式进行计算
    7. 检测一个数字是否基本类型 ?
  2. 问题:

    1. 如何使用@Controller 注解实现返回@RestController json结构
    2. 数据库连接池配置参数有那些
    3. redis使用的数据接口有那些,以及大key使用zset结构场景有那些

sql查询去重问题答案

1
2
3
SELECT a.id,a.user_icon from user_info a LEFT JOIN user_info b on a.user_icon = b.user_icon and a.id!=b.id WHERE b.id is not null and a.id<b.id
union all
select a.id,a.user_icon from user_info a where a.user_icon not in (SELECT user_icon from user_info GROUP BY user_icon HAVING count(1)>1)

美的面试问题整理

  1. spring的事务问题

    1. 事务的隔离级别 5种: ISOLATION_DEFAULT 默认,使用后端数据库的默认隔离级别,Mysql的默认是可重复读
    2. ISOLATION_READ_UNCOMMITTED 读未提交
    3. ISOLATION_READ_COMMITTED 读已提交
    4. ISOLATION_REPEATABLE_READ 可重复读
    5. ISOLATION_SERIALIZABLE 串行化

    事务的传播机制,就是当多个事务同事存在的情况 Spring 如何处理这些事务的行为

    1. Required 默认,如果没有当前事务,就创建一个事务,如果存在一个事务,就加入到这个事务中
    2. SUPPORTS 支持当前事务,如果没有当前事务,就以非事务方法执行
    3. Mandatory 使用当前事务,如果没有当前事务,就抛出异常。
    4. Required_new 新建事务,如果存在当前事务,就把当前事务挂起
    5. Not_supported 以非事务方式执行操作,如果当前事务存在则抛出异常
    6. Nested 如果当前存在事务,则在事务内执行,如果当前没有事务,则执行与Required 类型的操作。
    7. 默认的传播机制: PROPAFATION_REQUIRED
  2. 声明式事务失效情况

    1. @Transactional 应用在非Public 修饰的方法上。
    2. @Transactional 注解属性propagation 设置错误
    3. @Transactional 注解属性rollbackFor 设置错误
    4. 同一个类中方法调用,导致@Transactional 失效。
  3. Redis的持久化

  4. mysql的事务 间隙锁问题

美的电话面试问题:

  1. 分布式锁原理
  2. 分布式事务问题
  3. 分页组件是什么

Type的子类和子接口列表:

Class 实现 Type接口。

Class 是一个原始类型,Class的对象表示JVM的一个类或者接口,每个Java类在jvm都表现为一个Class对象

ParameterizedType 表示的是参数化类型,例如: List 等这种带有泛型的类型。

  1. Type getRawType()** 返回参数化类型中的原始类型,例如List 的原始类型为 List
  2. Type[] getActualTypeArguments() 获取参数化类型中的类型变量或者实际类型列表。 例如: Map<Integer,String> 的实际泛型列表是Integer,String 。 注意点: 返回类型是Type 可能存在多层嵌套情况。
  3. Type getOwnerType() 返回类型所属的类型

TypeVariable 表示的是类型变量,用来反映在JVM编译改泛型前的信息

例如: List<T> 中的T 就是类型变量,它在编译时需要转换成一个具体的类型才能正常使用。 

该接口常用方法有三个:

  1. Type[] getBounds() 获取类型变量的上边界(未声明情况默认为Object)
  2. D getGenericDeclaration() 获取声明类型变量的原始类型:例如:Class Test 的原始类型是 Test
  3. String getName() 获取在源码中定义的名字,上例中 就是 K

GenericArrayType 表示: 是数组类型且组成元素是: ParameterizedType或者TypeVariable

该接口方法:Type getGenericComponentType()
表示数组的组成元素

WildcardType 表示通配符泛型,例如: ?extends Number 或者 ? supper Integer

该接口有俩方法:

  1. Type[] getUpperBounds() 返回泛型变量的上界
  2. Type[] getLowerBounds() 返回泛型变量的下界

How to find the Entry with largest Value in a Java Map

  • Last Updated : 13 Sep, 2021

Read

Discuss

Given a map in Java, the task is to find out the entry in this map with the highest value.

Illustration:

1
2
3
4
Input  : Map = {ABC = 10, DEF = 30, XYZ = 20}
Output : DEF = 30
Input : Map = {1 = 40, 2 = 30, 3 = 60}
Output : 3 = 60

Methods: There can be several approaches to achieve the goal that are listed as follows:

  1. Simple iterative approach via for each loop
  2. Using max() method from Collections class
  3. Using the concept of Streams introduced in Java 8

Now we will discuss the above-proposed methods alongside procedure and implementing them via clean java programs.

Method 1: Using iterative approach for each loop

Approach:

  1. Iterate the map entry by entry
  2. Store the first entry in a reference variable to compare to initially.
  3. If the current entry’s value is greater than the reference entry’s value, then store the current entry as the reference entry.
  4. Repeat this process for all the entries in the map.
  5. In the end, the reference variable has the required entry with the highest value in the map.
  6. Print this entry
1
2
for (Map.Entry entry : map.entrySet()) 
{ // Operations }

Example:

  • Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
// Java program to Find Entry
// with the Highest Value in Map
// Using Comparators in Map interface

// Importing all utility classes
import java.util.*;

// Main class
class GFG {

// Method 1
// Find the entry with highest value
public static <K, V extends Comparable<V> >
Map.Entry<K, V>
getMaxEntryInMapBasedOnValue(Map<K, V> map)
{

// To store the result
Map.Entry<K, V> entryWithMaxValue = null;

// Iterate in the map to find the required entry
for (Map.Entry<K, V> currentEntry :
map.entrySet()) {

if (
// If this is the first entry, set result as
// this
entryWithMaxValue == null

// If this entry's value is more than the
// max value Set this entry as the max
|| currentEntry.getValue().compareTo(
entryWithMaxValue.getValue())
> 0) {

entryWithMaxValue = currentEntry;
}
}

// Return the entry with highest value
return entryWithMaxValue;
}

// Method 2
// To print the map
public static void print(Map<String, Integer> map)
{

System.out.print("Map: ");

// If map does not contain any value
if (map.isEmpty()) {

System.out.println("[]");
}
else {
System.out.println(map);
}
}

// Method 3
// Main driver method
public static void main(String[] args)
{

// Creating a Map
// Declaring object of string and integer type
Map<String, Integer> map = new HashMap<>();

// Inserting elements in the Map object
// using put() method
// Custom input element addition
map.put("ABC", 10);
map.put("DEF", 30);
map.put("XYZ", 20);

// Calling method 2 to
// print the map
print(map);

// Calling method 1 to
// find the entry with highest value and
// print on the console
System.out.println(
"Entry with highest value: "
+ getMaxEntryInMapBasedOnValue(map));
}
}

Output:

1
2
Map: {ABC=10, DEF=30, XYZ=20}
Entry with highest value: DEF=30

Method 2: Using max() method from Collections class without lambda expression

Example:

  • Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// Java Program to Find Entry with largest Value in Map
// Using max() method from Collections class

// Importing required classes
import java.util.*;

// main class
class GFG {

// Main driver method
public static void main(String[] args)
{

// Creating HashMap
// Declaring objects of string and integer type
HashMap<Integer, Integer> map
= new HashMap<Integer, Integer>();

// Inserting elements in the Map
// using put() method

// Custom input addition
map.put(1, 4);
map.put(2, 5);
map.put(3, 7);
map.put(4, 2);

// Using Collections.max() method returning max
// value in HashMap and storing in a integer
// variable
int maxValueInMap = (Collections.max(map.values()));

// Iterate through HashMap
for (Entry<Integer, Integer> entry :
map.entrySet()) {

if (entry.getValue() == maxValueInMap) {

// Print the key with max value
System.out.println(entry.getKey());
}
}
}
}

Output:

1
3

Method 3: Using the concept of Streams introduced in Java 8

  • Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// Java Program to Find Entry with largest Value in Map
// Using concept of Streams
import java.util.stream.*;

// Main class
class GFG {

// Method 1
public static void main(String[] args)
{

// Entries in our map
Map<String, String> map = new HashMap<>();

map.put("A", "23");
map.put("F", "43");
map.put("C", "56");
map.put("Z", "04");

// Printing the largest value in map by
// calling above method
System.out.print(map.maxUsingStreamAndLambda());
}

// Method 2
public <String, String>
extends Comparable<String> > maxUsingStreamAndLambda(
Map<String, String> map)
{

// Using lambda operation over streams
Map<String, String> maxEntry
= map.entrySet().stream().max(
(String e1, String e2)
-> e1.getValue().compareTo(
0e2.getValue()));

// Returning the maximum element from map
return maxEntry.get().getValue();
}
}

Output:

1
C