GithubHelp home page GithubHelp logo

blogs's Introduction

blogs's People

Contributors

lq920320 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

blogs's Issues

2019/05/29--Spring中几个注解的简单说明

本文打算介绍几个不太容易说出其区别,或者用途的 Spring 注解,比如 @Component@Bean 的比较,@ControllerAdvice 是如何处理自定义异常的等等。

Spring 中的一些注解

1. @Component@Bean 的区别是什么?

  1. 作用对象不同:@Component 注解作用于类,而 @Bean 注解作用于方法、
  2. @Component 通常是通过路径扫描来自动侦测以及自动装配到 Spring 容器中(我们可以使用 @ComponentScan 注解定义要扫描的路径从中找出标识了需要装配的类自动装配到 Springbean 容器中)。@Bean 注解通常是我们在标有该注解的方法中定义产生这个 bean@Bean 告诉了 Spring 这是某个类的实例,当我们需要用它的时候还给我。
  3. @Bean 注解比 @Component 注解的自定义性更强,而且很多地方我们只能通过 @Bean 注解来注册 bean。比如当我们引用第三方库中的类需要装配到 Spring 容器时,只能通过 @Bean 来实现。

@Bean 注解使用示例:

@Configuration
public class AppConfig {
    @Bean
    public TransferService transferService() {
        return new TransferServiceImpl();
    }
}

@Component 注解使用示例:

@Component
public class ServiceImpl implements AService {
    ....
} 

下面这个例子是通过 @Component 无法实现的:

@Bean
public OneService getService(status) {
    case (status)  {
        when 1:
                return new serviceImpl1();
        when 2:
                return new serviceImpl2();
        when 3:
                return new serviceImpl3();
    }
}

2. Autowire@Resource 的区别

  1. @Autowire@Resource都可以用来装配bean,都可以用于字段或setter方法。
  2. @Autowire 默认按类型装配,默认情况下必须要求依赖对象必须存在,如果要允许 null 值,可以设置它的 required 属性为 false。
  3. @Resource 默认按名称装配,当找不到与名称匹配的 bean 时才按照类型进行装配。名称可以通过 name 属性指定,如果没有指定 name 属性,当注解写在字段上时,默认取字段名,当注解写在 setter 方法上时,默认取属性名进行装配。

注意:如果 name 属性一旦指定,就只会按照名称进行装配。

  • @Autowire@Qualifier配合使用效果和@Resource一样:

    @Autowired(required = false) @Qualifier("example")
    private Example example;
    
    @Resource(name = "example")
    private Example example;
  • @Resource 装配顺序

  1. 如果同时指定 name 和 type,则从容器中查找唯一匹配的 bean 装配,找不到则抛出异常;
  2. 如果指定 name 属性,则从容器中查找名称匹配的 bean 装配,找不到则抛出异常;
  3. 如果指定 type 属性,则从容器中查找类型唯一匹配的 bean 装配,找不到或者找到多个抛出异常;
  4. 如果不指定,则自动按照 byName 方式装配,如果没有匹配,则回退一个原始类型进行匹配,如果匹配则自动装配。

3. 将一个类声明为 Spring 的 bean 的注解有哪些?

  • @Component :通用的注解,可标注任意类为 Spring 的组件。如果一个 Bean 不知道属于哪个层,可以使用 @Component 注解标注。
  • @Repository :对应持久层即 Dao 层,主要用于数据库相关操作。
  • @Service :对应服务层,主要设计一些复杂的逻辑,需要用到 Dao 层。
  • @Controller :对应 Spring MVC 控制层,主要用来接受用户请求并调用 Service 层返回数据给前端页面。
  • @Configuration :声明该类为一个配置类,可以在此类中声明一个或多个 @Bean 方法。

4. @Configuration :配置类注解

@Configuration 标明在一个类里可以声明一个或多个 @Bean 方法,并且可以由 Spring 容器处理,以便在运行时为这些 bean 生成 bean 定义和服务请求,例如:

@Configuration
public class AppConfig {

    @Bean
    public MyBean myBean() {
        // instantiate, configure and return bean ...
    }
}

我们可以通过 AnnotationConfigApplicationContext 来注册 @Configuration 类:

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(AppConfig.class);
ctx.refresh();
MyBean myBean = ctx.getBean(MyBean.class);
// use myBean ...

另外也可以通过组件扫描(component scanning)来加载,@Configuration 使用 @Component 进行原注解,因此 @Configuration 类也可以被组件扫描到(特别是使用 XML 的 <context:component-scan /> 元素)。@Configuration 类不仅可以使用组件扫描进行引导,还可以使用 @ComponentScan 注解自行配置组件扫描:

@Configuration
@ComponentScan("com.acme.app.services")
public class AppConfig {
    // various @Bean definitions ...
}

使用 @Configuration 的约束:

  • 配置类必须以类的方式提供(比如不能是由工厂方法返回的实例)。
  • 配置类必须是非 final 的。
  • 配置类必须是非本地的(即可能不在方法中声明),native 标注的方法。
  • 任何嵌套的配置类必须声明为 static
  • @Bean 方法可能不会反过来创建更多的配置类。

除了单独使用 @Configuration 注解,我们还可以结合一些外部的 bean 或者注解共同使用,比如 Environment API,@PropertySource@Value@Profile 等等许多,这里就不做详细介绍了,更多的用法可以参看 Spring @Configuration 的相关文档

5. @ControllerAdvice :处理全局异常利器

在 Spring 3.2 中,新增了 @ControllerAdvice@RestControllerAdvice@RestController 注解,可以用于定义 @ExceptionHandler@InitBinder@ModelAttribute,并应用到所有 @RequestMapping@PostMapping@GetMapping等这些 Controller 层的注解中。

默认情况下,@ControllerAdvice 中的方法应用于全局所有的 Controller。而使用选择器 annotations()basePackageClasses()basePackages() (或其别名value())来定义更小范围的目标 Controller 子集。 如果声明了多个选择器,则应用 OR 逻辑,这意味着所选的控制器应匹配至少一个选择器。 请注意,选择器检查是在运行时执行的,因此添加许多选择器可能会对性能产生负面影响并增加复杂性。

@ControllerAdvice 我们最常使用的是结合 @ExceptionHandler 用于全局异常的处理。可以结合以下例子,我们可以捕获自定义的异常进行处理,并且可以自定义状态码返回:

@ControllerAdvice("com.developlee.errorhandle")
public class MyExceptionHandler {
    /**
     * 捕获CustomException
     * @param e
     * @return json格式类型
     */
    @ResponseBody
    @ExceptionHandler({CustomException.class}) //指定拦截异常的类型
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) //自定义浏览器返回状态码
    public Map<String, Object> customExceptionHandler(CustomException e) {
        Map<String, Object> map = new HashMap<>();
        map.put("code", e.getCode());
        map.put("msg", e.getMsg());
        return map;
    }
}

更多信息可以参看 Spring @ControllerAdvice 的官方文档

6. @Component, @Repository, @Service 的区别

注解 含义
@component 最普通的组件,可以被注入到 Spring 容器进行管理
@repository 作用于持久层
@service 作用于业务逻辑层
@controller 作用于表现层(spring-mvc的注解)

@Component是一个通用的Spring容器管理的单例bean组件。而@Repository, @Service, @Controller就是针对不同的使用场景所采取的特定功能化的注解组件。

因此,当你的一个类被@component所注解,那么就意味着同样可以用@Repository, @Service, @Controller 来替代它,同时这些注解会具备有更多的功能,而且功能各异。

最后,如果你不知道要在项目的业务层采用@Service还是@Component注解。那么,@Service是一个更好的选择。

总结

以上简单介绍了几种 Spring 中的几个注解及代码示例,就我个人而言,均是平时用到且不容易理解的几个,或者容易忽略的几个。当然,这篇文章并没有完全介绍完,在今后还会继续补充完善。

链接

2017/11/24--《代码大全》重构章节部分内容

  1. 软件演化的类型
  2. 重构简介
  3. 特定的重构
  4. 安全的重构
  5. 重构策略

神话: 一个管理很完善的软件项目,应该首先以系统化的方法进行需求开发,定义一份严谨的列表来描述程序的功能。设计完全遵循需求,并且完成得相当仔细,这样就让程序员的代码编写工作能够从头至尾直线型地工作。这也表明绝大多数代码编写后就已完美,测试通过后即可被抛到脑后。如果这样的神话是真的,那么代码被修改的唯一时机就是在软件维护阶段,而这一阶段只会在系统的最初版本交付用户之后。

现实情况: 在初始开发阶段,代码会有实质性的进化。在初始代码编写过程中,就会出现很多剧烈的改变,如同在代码维护阶段可以看到的那样。根据项目的规模不同,典型的项目花在编码、调试和单元测试上的时间会占到整个项目的30%到65%不等。如果代码编写和单元测试能够一帆风顺,这两个阶段所占项目时间的比例不会超过20%到30%。即使是管理完善的项目,每个月都有大约1/4的需求发生变化。需求的变化将不可避免地导致相关代码的改变——有时是实质性的代码改变。

另一个事实: 现在的开发增强了代码在构造阶段中改变的潜力。在旧式的软件生命周期中,项目成功与否的关键在于能否避免代码的改变。越来越多的现代开发方法已经放弃了对代码的前瞻性。如今的开发方法更多地以代码为中心,在整个项目生命周期中代码都会不断地演化。你可以期望代码的演化比以往任何时候更频繁。简单来讲,在新式的软件生命周期中,项目成功与否的关键在于能否应对需求的剧变。

软件演化的类型

软件演化就像生物进化一样,有些突变对物种是有益的,另外一些则是有害的。良性的软件演化使得代码得到了发展,就如猴子进化到穴居人再进化到我们人类。然而,有时演化的力量也会以另一种方式打击你的程序,甚至将它送入不断退化的螺旋形轨道。
区分软件演化类型的关键,就是程序的质量在这一过程中是提高了还是降低了。
区分软件演化类型的第二个标准,就是这样的演化是源于程序构建过程中的修改,还是维护过程中的修改。构建中的修改通常是由最初的开发人员完成,在这一阶段程序还没有被人们彻底遗忘。这时系统也未上线待售,因此,完成修正压力仅仅是来自于时间表——绝不会有500个愤怒的用户质问你为什么他们的系统会崩溃。出于同样的原因,构建期间的修改常常是随心所欲之作——系统处于高度动态阶段,出现错误的代价较小。这样的环境孕育着与维护期不同的软件演化风格。
软件演化的基本准则就是,演化应当提升程序的内在质量。
再庞大复杂的代码都可以通过重构加以改善。 ——Gerald Weinberg

重构简介

要实现软件演化基本准则,最关键的策略就是重构,Martin Fowler 将其定义为“在不改变软件外部行为的前提下,对其内部结构进行改变,使之更容易理解并便于修改”(Fowler 1999)。在现代编程理论中,“重构(refactoring)”一词源于Larry Constantine 在结构化程序设计中首次使用的“factoring”。当时指尽可能地将一个程序分解为多个组成部分。

1. 重构的理由

有时,代码在维护过程中质量会降低,而有时代码在最初诞生的时候就先天不良。

代码重复 重复的代码几乎总是代表着对最初设计里彻底分解方面的一个失误。无论何时,如果需要对某个地方进行修改,你都不得不在另一个地方完成同样的修改——重复代码总会将你置于一种两线作战的尴尬境地。重复的代码同样违背了Andrew Hunt 和 Dave Thomas 所提出的“DRY原则”:不要重复自己“Don't Repeat Yourself”(Hunt and Thomas 2000)。我想还是David Parnas说得最为精辟:“复制粘贴即设计之缪。”

冗长的子程序 在面向对象的编程中,很少会需要用到长度超过一个屏幕的子程序。这样的子程序通常暗示程序员是在把一个结构化程序的脚塞进一只面向对象的鞋子里。

循环过长或嵌套太深 循环内部的复杂代码常常具备转换为子程序的潜质,这样的改动将有助于对代码的分解,并减少循环的复杂性。

内聚性太差的类 如果看到有某个类大包大揽了许多彼此无关的任务,那么这个类就该被拆分成多个类,每个类负责一组具有内在的相互关联的任务。

类的接口未能提供层次一致的抽象 即使是那些从诞生之日起就具有内聚接口的类也可能渐渐失去最初的一致性。

拥有太多参数的参数列表 如果一个程序被分解得很好,那么它的子程序应当小巧、定义精确,且不需要庞大的参数列表。

类的内部修改往往被局限于某个部分 有时一个类会有这两种或更多独立的功能。如果你发现自己要么修改类里的一部分,要么修改另一部分,但极少的修改会同时影响类中的两个部分,这就表明该类应该根据独立的功能被拆分为多个类。

变化导致对多个类的相同修改 如果发现自己常常对同一组类进行修改,这表明这些类中的代码应当被重新组织,使修改仅影响到其中的一个类。

对继承体系的同样修改 每次为某个类添加派生类时,都会发现自己不得不对另一个类做同样的操作。

case语句需要做相同的修改 尽管使用case语句本身不是坏事,但如果不得不在程序的多个部分里对类似的一组case语句做出相同的修改,那么就应当问问自己,使用继承是否是更明智的选择。

同时使用的相关数据并未以类的方式进行组织 如果看到自己常常对同样的一组数据进行操作,是否改将这些数据及操作组织到一个类里面。

成员函数使用其他类的特征比使用自身类的特征还要多 这一状况暗示着这一子程序应该被放到另一个类中,然后再原来的类里调用。

过多使用基本数据类型 基本数据类型可用于表示真实世界中实体的任意数量,如果程序中使用了整型这样的基本数据类型表示某种常见的实体,如货币,请考虑创建一个简单的Money类,这样编译器就可以对Money变量执行类型检查,你也可以对赋给Money的值添加安全检查等功能。

某个类无所事事

一系列传递流浪数据的子程序 看看自己的代码,把数据传递给某个子程序,是否仅仅就是为了让该子程序把数据转交给另一个子程序。这样传来传去的数据被称为“流浪数据/tramp data”。这样做并非错误,只是需要自检,如此传递特定数据,是否与每个子程序接口所表示的抽象概念一致。

中间人对象无事可做 如果看到某个类中的绝大部分代码只是去调用其他类中的成员函数,请考虑是否应该把这样的中间人(middleman)去掉,转而直接调用其他的类。

某个类同其他类关系过于亲密 如果需要使程序具备更强的可管理性,并最大限度地减少更改代码对周围的连带影响,那么封装(信息隐藏)可能是最强有力的工具了。只要发现某个类对另一个类的了解程度超过了应该的程度——包括派生类了解基类中过多的东西,那么宁可让代码因较强的封装而出错,也不要减弱封装。

子程序命名不当

数据成员被设置成公用 这样会模糊接口和实现之间的接线,其本身也违背了封装的原则,限制了类在未来可以发挥的灵活性。因此,请认真考虑把public数据成员藏在访问器子程序背后。

某个派生类仅使用了基类的很少一部分成员函数 因此应当考虑进行更完善的封装:把派生类相对于基类的关系从“is-a”转变为“has-a”。即把基类转换成原来的派生类的数据成员,然后仅仅为原来的派生类提供所需要的函数。

注释被用于解释难懂的代码 注释在程序中扮演了重要的角色,但它不应当被用来为拙劣代码的存在而辩护。有箴言为证:“不要为拙劣的代码编写文档——应当重写代码”(Kernighan and Plauger 1978)。

使用了全局变量 当你再度遇到某段使用了全局变量的代码时,请花点时间来重新检查一下这些代码。

在子程序调用前使用了设置代码(setup code),或在调用后使用了收尾代码(takedown code)

//C++示例:在子程序调用前后的设置代码和收尾代码——糟糕的做法
//在调用子程序之前的设置代码
WithdrawalTransaction withdrawal;
withdrawal.SetCustormerId( custormerId );
withdrawal.SetBalance( balance );
withdrawal.SetWithdrawalAmount( withdrawalAmount );
withdrawal.SetwithdrawalDate( withdrawalDate );

ProcessWithdrawal( withdrawal );

//在调用子程序之后的收尾代码
customerId = withdrawal.GetCustomerId();
balance = withdrawal.GetBalance();
withdrawalAmount = withdrawal.GetWithdrawalAmount();
withdrawalDate = withdrawal.GetWithdrawalDate();

程序中的一些代码似乎是在将来的某个时候才会用到的 在猜测程序将来有哪些功能可能被用到这方面,程序员已经声名狼藉了。

  • 对这些“超前设计”的代码而言,需求不可能定义得很完备。这就意味着程序员对于未来需求的猜测很可能是错误的。
  • 即使程序员对未来需求的前瞻几近完全正确,他也不可能广泛预见未来需求所有的复杂脉络。这些错综复杂的关系将会埋葬程序员的基本设计构思。
  • 那些使用“超前设计”代码的未来程序员们并不知道自己手中的代码原本是经过“超前设计”的,或许他们会期望这些代码能比实际情况表现得更好。
  • “超前设计”的代码是画蛇添足,增加了程序的复杂性,带来了额外的测试、修补缺陷等工作量。其整体效应就是拖了项目的后腿。

2.拒绝重构的理由

在日常的讨论中,“重构”一词更多被用来指那些弥补缺陷、增加功能、修改设计等工作,全然成为了对代码做了任何修改的同义词。这一术语的深刻内涵已惨遭稀释。修改本身并不是什么了不得的好事,但如果是程序员深思熟虑而为之,且遵循规范恰如其分,那么在不断的维护下,这样的修改必将成为代码质量稳步提升之关键,且能避免如今随处可见的代码因质量不断下降而最终灭亡的趋势。

特定的重构

1.数据级的重构

用具名常量替代神秘数值

使变量的名词更为清晰且传递更多信息

将表达式内联化 把一个中间变量换成给它赋值的那个表达式本身。

用函数来代替表达式 用一个函数来代替表达式(这样一来,表达式就不会在代码中重复出现了)。

引入中间变量 要记住,给这个中间变量命名应能准确概括表达式的用途。

用多个单一用途的变量代替某个多用途变量 如果某个变量身兼数职——通常是i、j、temp、x——请用多个变量来让它们各司其职吧,各个变量还应该具有更为准确的变量名。

在局部用途中使用局部变量而不是参数 如果一个被用作输入的子程序参数在其内部又被用作局部变量,那么请直接创建一个局部变量来代替它。

将基础数据类型转化为类 如果一个基础数据类型需要额外的功能(例如更为严格的类型检查)或额外的数据,那么就把该数据转换为一个对象,然后再添加你所需要的类行为。

将一组类型码(type codes)转化为类或枚举类型

将一组类型码转换为一个基类及其相应的派生类 如果与不同类型相关联的不同代码片段有着不一样的功能,请考虑为该类创建一个基类,然后针对每个类型码创建派生类。例如对OutputType基类,就可以创建Screen、Printer和File这样的派生类。

将数组转换为对象 如果正在使用一个数组,其中的不同元素具有不同的类型,那么就应该用一个对象来替代它。将数组中的各个元素转化为该类的各个成员。

把群集(collection)封装起来 如果一个类返回一个群集,到处散布的多个群集实例将会带来同步问题。请让你的类返回一个只读群集,并且提供相应的为群集添加和删除元素的子程序。

用数据类来代替传统记录 建立一个包含记录成员的类。这样你可以集中完成对记录的错误检查、持久化和其他与该记录相关的操作。

2.语句级的重构

分解布尔表达式 通过引入命名准确的中间变量来简化复杂的布尔表达式,通过变量名更好地说明表达式的含义。

将复杂布尔表达式转换成命名准确的布尔函数

合并条件语句不通部分中的重复代码片段 如果你有完全相同的代码同时出现在一个条件语句中的if语句块和else语句块中,那么就应该讲这段代码移到整个if-then-else语句块的后面。

使用break或return而不是循环控制变量 如果在循环中用到了一个类似done这样的控制循环的变量,请使用break或return来代替它。

在嵌套的if-then-else语句中一旦知道答案就立刻返回,而不是去赋一个返回值

用多态来替代条件语句(尤其是重复的case语句) 结构化程序里很多的case语句中的逻辑都可以被放到继承关系中,通过多态函数调用实现。

创建和使用null对象而不是去检测空值 有时,null对象可以有一些相关的通用功能或数据,诸如引用一个不知名字的resident对象时把它作为“occupant”。遇到这种情况,应该把处理null值的功能从客户代码中提出来,放到相应的类中。做法如下:设计一个Customer类,在resident未知时将其定义为“occupant”;而不是让Customer类的客户代码反复检测对象的名字是否已知,并在未知时用“occupant”代替它。

3.子程序级重构

提取子程序或者方法 把内嵌的代码(inline code)从一个子程序中提取出来,并将其提炼为单独的子程序。

将子程序的代码内联化 如果子程序的程序体很简单,且含义不言自明,那么就在使用的时候直接使用这些代码。

将冗长的子程序转换为类 如果子程序太长,可以将其转换为类,然后进一步对之前的子程序进行分解,通过所得到的多个子程序来改善该代码的可读性。

用简单的算法替代复杂算法

增加参数 如果子程序需要从调用方获得更多的信息,可以增加它的参数从而为其提供信息。

删除参数 如果子程序已经不再使用某个参数,就删掉它。

将查询操作从修改操作中独立出来 通常,查询操作并不改变对象的状态。一次,一旦有了类似GetTotals()的操作改变了对象的状态,就应该将查询功能从状态改变中独立出来,提供两个独立的子程序。

合并相似的子程序,通过参数区分它们的功能 两个相似子程序唯一区别或许只是其中用到的常量值不同。请把它们合并到一起,然后将常量值通过参数传入。

将行为取决于参数的子程序拆分开来 如果一个子程序根据输入参数的值执行了不同的代码,请考虑将它拆分成几个可以被单独调用的、无须传递特定参数的子程序。

传递整个对象而非特定成员 如果发现有同一个对象的多个值被传递给了一个子程序,考虑是否可修改其接口使之接受整个对象。

传递特定成员而非整个对象

包装向下转型的操作 通常当子程序返回一个对象时,应当返回其已知的最精确的对象。这尤其适用于返回迭代器、群集、群集元素等的情况。

4.类实现的重构

将值对象转化为引用对象 如果发现自己创建并维护着多个一模一样的大型复杂对象,请改变对这些对象的使用方式。即仅仅保留一份主拷贝(值对象),然后其他地方使用对该对象的引用(引用对象)。

将引用对象转化为值对象 如果看到自己对某个小型的简单对象进行了多次引用操作,请将这些对象都设置为值对象。

用数据初始化替代虚函数 如果有一组派生类,差别仅仅是虚函数返回的常量不同。与其派生类中覆盖成员函数,不如让派生类在初始化时设定适当的常量值,然后使用基类中的通用代码处理这些值。
指向基类的指针在操作它的多态类对象时,会根据不同的类对象,调用其相应的函数,这个函数就是虚函数。

改变成员函数或成员数据的位置 请考虑对类的继承体做出修改。这些修改通常可以减少派生类的重复工作:

  • 将子程序上移到基类中。
  • 将成员上移到基类中。
  • 将构造函数中的部分代码上移到基类中。
    下面这些改变则可以用来对派生类进行特殊化:
  • 将子程序下移到派生类中。
  • 将成员下移派生类中。
  • 将构造函数下移到派生类中。

将特殊代码提取为派生类 如果某类中的一部分代码仅仅被其部分实例所使用,应该把这部分特殊的代码放到其派生类中。

将相似的代码结合起来放置到基类中 如果两个派生类中有相似的代码,将这些代码结合起来并放到基类中。

5.类接口的重构

将成员函数放到另一个类中 在目标类中创建一个新的成员函数,然后从原类中将函数体移到目标类中。然后再旧的成员函数中调用新的成员函数。

将一个类变成两个 如果一个类同时具备两种或更多的截然不同的功能,请把这个类转化为多个类,使得每个类完成一种明确定义的功能。

删除类 如果某个类无所事事,就应该把该类的代码放到与所完成功能关系更为密切的另一个类中,然后把这个类删掉。

去除委托关系 有时类A调用了类B和类C,而实际上类A只应该调用类B,而B类应该调用类C。在这种情况下就应当考虑A对B的接口抽象是否合适。如果应该由B负责调用C,那么就应该只有B调用C。

去掉中间人 如果存在类A调用类B,类B调用类C的情况,有时让类A直接调用类C会更好。是否应当去掉类B,取决于怎么做才能最好地维护类B接口的完整性。

用委托代替继承 如果某类需要用到另一个类,但又打算获取该类接口更多的控制权,那么可以让基类成为原派生类的一个成员,并公开它的一组成员函数,以完成一种内聚的抽象。

用继承代替委托 如果某个类公开了委托类(成员类)所有成员函数,那么该类应该从委托类继承而来,而不是使用该类。

引入外部的成员函数 如果一个客户类需要被调用类的某个额外的成员函数,而你又无法去修改被调用类,那么可以通过在客户类(client class)中创建新成员函数的方式来提供此功能。

引入扩展类 如果一个类需要多个额外的成员函数,你同样无法修改该类,你可以创建一个新类。该类包括了原类的功能以及新增加的功能。要实现这点,你既可通过原类派生新类然后添加新的成员函数,也可以将原类进行包装,使新类调用所需要的成员函数。

对暴露在外的成员变量进行封装 如果数据成员是公用的,请将其改为私用,然后通过成员函数来访问该数据成员的值。

对于不能修改的类成员,删除相关的Set()成员函数

隐藏那些不会在类之外被用到的成员函数

封装不会使用的成员函数 如果发现自己往往只使用类接口的一部分,那么就为类创建新的接口,仅仅把那些必须的成员函数暴露给类的外部,需要注意,新的接口应该为类提供一致的抽象。

合并那些实现非常类似的基类和派生类 如果派生类并未提供更多的特殊化,那么就应该把它合并会基类。

6.系统级重构

为无法控制的数据创建明确的索引源 有时,你需要让特定系统来维护数据,而在其他需要使用该数据的对象中,你却无法方便或一致地访问这些数据。常见的例子如在GUI控件中维护的数据。在这样的情况下,你需要创建一个类,由该类里映射GUI控件中的数据,然后让GUI控件和其他代码将此类作为该数据的明确来源。

将单向的类联系改为双向的类联系 如果你有两个类,且它们各自需要用到对方的功能,但仅有一个类能访问另一个类。这时就应该将对两个类进行修改,使其相互调用。

将双向的类联系改为单向的类联系 如果有两个类,彼此都知道对方,但实际上只有一个类需要访问另一个类。这时就应该只让那个有实际需要的类能访问另一个类,而另一个类无法访问该类。

用Factory Method方式而不是简单地构造函数 在需要基于类型码创建对象,或者希望使用引用对象而非值对象的时候,应当使用Factory Method(函数)。

用异常取代错误处理代码,或者做相反方向的变换

安全的重构

与其将分解一个正常工作的系统比作替换水槽里面的塞子,倒不如把它看成是替换大脑中的一根神经。如果我们把软件维护称为“软件脑部外科手术”,工作起来会不会要轻松一些?
——Gerald Weinberg

保存初始代码 在开始重构之前,要保证你还能回到代码的初始状态。

重构的步伐请小些 有的重构的步伐比其他重构更大,到底什么能算成是一次重构并不明确。因此请把重构的步伐放小些,这样才能理解所做修改对程序的全部影响。

同一时间只做一项重构 有的重构会比其他的重构更为复杂。除非是对那些最为简单的重构,否则请在同一时间只做一项重构,在进入下一项重构之前,对代码重新编译并测试。

把要做的事情一条条列出来

设置一个停车场 在某次重构的路途上,你可能会发现你需要进行另一次重构。正在着手这次新的重构时,或许又发现第三个重构会给程序带来很多好处。为了处理这些并不需要立即对付的修改工作,你最好设置一个“停车场”,把你需要在未来某个时间进行而现在可以先放在一边的修改工作列出来。

多使用检查点 在重构的时候,很容易出现代码没有按照设想正常运行的情况。除了保存初始代码外,在重构中还应在多个地方设置检查点。这样一来,即使你编码时钻进了死胡同,你仍然可以让程序回到正常工作的状态。

利用编译器警告信息

重新测试 应该把重新测试作为检查所修改代码工作的补充。当然,这点要取决于从一切开始你是否就有一套优秀的测试用例。

增加测试用例 除了重新运行过去做过的那些测试,还应该增加新的单元测试来检验新引入的代码。如果重构是的一些测试用例已经过时,那么就删除这些用例。

检查对代码的修改 如果说在第一次运行程序的时候检查代码是必需的,那么在接下来的修改工作中,时刻关注代码则更为重要。当代码修改行数从1增加到5的时候,改错的可能性大大增加。在这之后,随着行数的增加,出错的几率可是逐渐降低了。
程序员对于很小的修改常常不以为然。他们不会用纸和笔来推敲程序,也不会让其他人来检查代码。有时甚至根本不会运行这些代码来验证修改工作的正确性。

根据重构风险级别来调整重构方法 对于那些有一定风险的重构,谨慎才能避免出错。务必一次只处理一项重构。除了完成通常要做的编译检查和单元测试之外,还应该让其他人来检查你的重构工作,或是针对重构采用结对编程。

不宜重构的情况

不要只实现一部分功能,并指望将来的重构能完成它。——John Manzo
重构是一剂良药,但不是包治百病的灵丹妙药。
不要把重构当做先写后改的代名词 重构最大的问题在于被滥用。程序员们有时会说自己是在重构,而实际上他们所完成的工作仅仅是对无法运行的代码修修补补,希望能让程序跑起来。重构的含义是在不影响程序行为的前提下改进可运行的代码。那些修补破烂代码的程序员们不是在重构,而是在拼凑代码(hacking)。

避免用重构代替重写 有时,代码所需要的不是细微修改,而是直接一脚踢出门外,这样你就可以全部重新开始。如果发现自己处于大规模的重构之中,就应该问问自己是否应该把这部分代码推倒重来,重新设计,重新开发。

重构策略

对任何特定程序都能带来好处的重构方法本应是无穷无尽的。和其他编程行为一样,重构同样受制于收益递减定律,同样也符合80/20法则。在斟酌哪种重构方法最为重要的时候,不妨考虑一下下面这些建议。

收益递减规律是指其他投入固定不变时,连续地增加某一种投入,所新增的产出最终会减少的规律。该规律另一种等价的说法是:超过某一水平之后边际投入的边际产出下降。
二八定律又名80/20定律、帕累托法则(定律)也叫巴莱特定律、最省力的法则、不平衡原则等,是19世纪末20世纪初意大利经济学家巴莱多发现的。他认为,在任何一组东西中,最重要的只占其中一小部分,约20%,其余80%尽管是多数,却是次要的,因此又称二八定律。

在增加子程序时进行重构 在增加子程序时,检查一下相关的子程序是否都被合理地组织起来了。如果没有,那么就重构这些子程序。

在添加类的时候进行重构 添加一个类往往会使已有代码中的问题浮出水面。

在修补缺陷的时候进行重构 如果你在修补缺陷中有了一些心得体会,请把它运用到改善其他易于产生相似错误的代码上。

关注易于出错的代码 有的模块更容易出错,健壮性远逊于其他模块。尽管绝大部分人对这部分富于挑战性代码的自然反应都会是敬而远之,但集中处理这样的代码将是最为有效的重构策略。

关注高度复杂的模块 另一种方法就是关注最为复杂的模块。一项经典研究表明,当做维护的程序员们把改善代码的精力放在那些最为复杂的模块上时,程序的质量会有显著提升。

在维护环境下,改善你手中正在处理的代码 未予修改的代码是没有必要进行重构的。但如果你正在维护某部分代码,请确保代码在离开你的时候比来之前更健康。

定义清楚干净代码和拙劣代码之间的边界,然后尝试把代码移过这条边界 “现实世界”通常会比你想象的更加混乱。这种状态或是源于复杂的业务规则,或是来自软硬件接口。对那些古董系统而言,常见的麻烦就是人们会要求那些拙劣编写的产品代码自始至终都能工作下去。

  1. 真实世界混乱不堪并不等于你的代码也得同样糟糕。将你的系统看做理想世界、混乱的真实世界,以及从前者到后者的接口的结合。
  2. 改善产品代码的策略之一就是在拿到拙劣的遗产代码时对其重构,由此使其告别混乱不堪的真实世界。
    在你处理这一系统时,你可以设法把代码移过“真实世界接口”,转移到更为有序的理想世界中。在处理一个旧系统时,几乎整个系统可能都是那些写得非常糟糕的代码拼凑起来的。当你对付某段混乱的代码的时候,一种屡试不爽的办法是使这些代码靠近当前的代码规范,例如使用含义明确的变量名等,从而有效地将这部分代码引入到理想世界中。一次次这样处理下去,代码的基础质量就能迅速提升。

Key Points

  • 修改是程序一生都要面对的事情,不仅包括最初的开发阶段,还包括首次发布之后。
  • 在修改中软件的质量要么改进,要么恶化。软件演化的首要法则就是代码演化应当提升程序的内在质量。
  • 重构成功之关键在于程序员应学会关注那些标志着代码需要重构的众多的警告或“代码臭味”。
  • 重构成功另一要素是程序员应当掌握大量特定的重构方法。
  • 重构成功的最后要点在于要有安全重构策略。一些重构方法会比其他重构方法要好。
  • 开发阶段的重构是提升程序质量的最佳时机,因为你可以立刻让刚刚产生的改变梦想变成现实。请珍惜这些开发阶段的天赐良机!

2019/03/06--关于幂等的理解

什么是幂等?以及你是怎么理解的?

定义

幂等(idempotent、idempotence) 是一个数学与计算机学概念,常见于抽象代数中。
在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。例如,“setTrue()”函数就是一个幂等函数,无论多次执行,其结果都是一样的.更复杂的操作幂等保证是利用唯一交易号(流水号)实现。

如何理解幂等性

HTTP中的幂等性

HTTP GET 方法用于获取资源,不应有副作用,所以是幂等的。 请注意,这里强调的是一次和N次具有相同的副作用,而不是每次GET的结果相同。
HTTP DELETE方法用于删除资源,有副作用,但它应该满足幂等性。 比如:DELETE http://www.forum.com/article/4231,调用一次和N次对系统产生的副作用是相同的,即删掉id为4231的帖子;因此,调用者可以多次调用或刷新页面而不必担心引起错误。
HTTP POST方法用于创建资源,所对应的URI并非创建的资源本身,而是去执行创建动作的操作者,有副作用,不满足幂等性。 两次相同的POST请求会在服务器端创建两份资源,它们具有不同的URI;所以,POST方法不具备幂等性。
HTTP PUT方法用于创建或更新操作,所对应的URI是要创建或更新的资源本身,有副作用,它应该满足幂等性。 对同一URI进行多次PUT的副作用和一次PUT是相同的;因此,PUT方法具有幂等性。

分布式系统中幂等性

简单来讲,就是分布式系统需要提供具有幂等性的对外接口,即用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。保证最终的结果一致性。
比如支付,用户购买商品使用约支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额返发现多扣钱了,流水记录也变成了两条。这种情况是不允许出现的。
在以前的单应用系统中,我们只需要把数据操作放入事务中即可,发生错误立即回滚,但是再响应客户端的时候也有可能出现网络中断或者异常等等。
那么如何设计具有幂等性的接口呢?
方法一:单次支付请求,也就是直接支付了,不需要额外的数据库操作了,这个时候发起异步请求创建一个唯一的ticketId,就是门票,这张门票只能使用一次就作废,具体步骤如下:
1、异步请求获取门票
2、调用支付,传入门票
3、根据门票ID查询此次操作是否存在,如果存在则表示该操作已经执行过,直接返回结果;如果不存在,支付扣款,保存结果
4、返回结果到客户端
如果步骤4通信失败,用户再次发起请求,那么最终结果还是一样的。

方法二:分布式环境下各个服务相互调用
这边就要举例我们的系统了,我们支付的时候先要扣款,然后更新订单,这个地方就涉及到了订单服务以及支付服务了。用户调用支付,扣款成功后,更新对应订单状态,然后再保存流水。而在这个地方就没必要使用门票ticketId了,因为会比较的麻烦(支付状态:未支付,已支付)
步骤:
1、查询订单支付状态
2、如果已经支付,直接返回结果
3、如果未支付,则支付扣款并且保存流水
4、返回支付结果
如果步骤4通信失败,用户再次发起请求,那么最终结果还是一样的。
对于做过支付的朋友,幂等也可以称之为冲正,保证客户端与服务端的交易一致性,避免多次扣款。

总结

保证系统的幂等性就是保证系统结果的最终一致性。

2018/12/21--Javascript判断两个对象是否相等

var objA = {
  id: 1,
  name: "AAA"
};

var objB = {
  id: 1,
  name: "AAA"
};

/**
 * 判断两个对象是否相等
 */
function isEquivalent(a, b) {
  // Create arrays of property names
  var aProps = Object.getOwnPropertyNames(a);
  var bProps = Object.getOwnPropertyNames(b);
  // If number of properties is different,
  // objects are not equivalent
  if (aProps.length != bProps.length) {
    return false;
  }
  for (var i = 0; i < aProps.length; i++) {
    var propName = aProps[i];
    // If values of same property are not equal,
    // objects are not equivalent
    if (a[propName] !== b[propName]) {
      return false;
    }
  }
  // If we made it this far, objects
  // are considered equivalent
  return true;
}

// true
console.log(isEquivalent(objA, objB));

2019/05/23--PropertyMapper的介绍

PropertyMapper初见

在浏览 github 的时候,偶然发现了 spring-boot 的源码中有这么一段代码:

public void applyTo(RepositoryRestConfiguration rest) {
    PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
    map.from(this::getBasePath).to(rest::setBasePath);
    map.from(this::getDefaultPageSize).to(rest::setDefaultPageSize);
    map.from(this::getMaxPageSize).to(rest::setMaxPageSize);
    map.from(this::getPageParamName).to(rest::setPageParamName);
    map.from(this::getLimitParamName).to(rest::setLimitParamName);
    map.from(this::getSortParamName).to(rest::setSortParamName);
    map.from(this::getDetectionStrategy).to(rest::setRepositoryDetectionStrategy);
    map.from(this::getDefaultMediaType).to(rest::setDefaultMediaType);
    map.from(this::getReturnBodyOnCreate).to(rest::setReturnBodyOnCreate);
    map.from(this::getReturnBodyOnUpdate).to(rest::setReturnBodyOnUpdate);
    map.from(this::getEnableEnumTranslation).to(rest::setEnableEnumTranslation);
}

嗯……这是什么用法,恕我见识浅薄,没怎么见过还有这么写的啊。看上去,像是对象属性值的复制,于是验证了一下,果然如此。另外,还学习了 PropertyMapper 的其他操作。

PropertyMapper 的用法

PropertyMapper 是可用于将值从提供的源映射到目标的实用程序,简单来讲,就是属性值的拷贝。是自 spring-boot 2.0 版本新增的功能。主要用于在从@ConfigurationProperties 映射到第三方类时提供帮助。

1. 对象的 setter 方法

PropertyMapper 可以用于 setter 方法来对目标对象进行赋值,不仅可以通过 Supplier<T> ,而且可以直接通过确定的值来进行赋值:

    @Test
    public void propertyMapperTest() throws JsonProcessingException {
        ObjectMapper objectMapper = new ObjectMapper();
        PropertyMapper mapper = PropertyMapper.get().alwaysApplyingWhenNonNull();
        Student student1 = new Student() {{
            setId(1);
            setName("TOM-007");
        }};
        System.out.println(objectMapper.writeValueAsString(student1));
        Student student2 = new Student();
        // from(Supplier<T> supplier)
        mapper.from(student1::getId).to(student2::setId);
        mapper.from(student1::getName).to(student2::setName);
        mapper.from(student1::getScore).to(student2::setScore);
        // from(T value)
        mapper.from(80).to(student2::setScore);
        System.out.println(objectMapper.writeValueAsString(student2));
    }

输出:

{"id":1,"name":"TOM-007","score":null}
{"id":1,"name":"TOM-007","score":80}

链接:

2019/02/19--ArrayList的默认容量是多少?

看到了一道比较有意思的面试题,之前并没有深入研究过,在这儿记录一下。

在 Java 中,ArrayList 的默认容量是10,而且每当其元素个数超过容量长度的时候,会自动进行扩容,扩容的大小为 原容量*0.5 + 1,比如原容量是10,一次扩容之后为16;同样的,Vector 的默认容量也是10,但其扩容大小是 原容量的一倍,即原容量是10,一次扩容之后为20;ArrayList以及Vector的加载因子都是1。

接着,HashMap的默认容量是多少呢?

HashMap 的默认容量大小为16,当然这个也可以在初始化Map的时候手动设置,但必须是2的幂,比如:

Map map = new HashMap(4); 

另外,HashMap的加载因子为0.75,即元素个数超过容量长度的0.75倍之后,便会进行扩容,扩容大小为原容量的一倍,比如HashMap的容量为16,一次扩容后是容量为32。

此外还有HashSet,线程不安全,存取速度快,底层实现是一个HashMap(保存数据),实现Set接口,默认初始容量为16,加载因子为0.75:即当 元素个数 超过 容量长度的0.75倍 时,进行扩容扩容增量:原容量的 1 倍,比如 HashSet的容量为16,一次扩容后是容量为32。

参考链接:

2019/12/20--对象中某属性的求和

public class ObjectPropertySumTest {


    @Data
    private class Student {
        private String name;
        private Integer age;
    }


    @Test
    public void objectPropertySum() {
        List<Student> students = buildTestData();
        // 7
        System.out.println(students.size());
        // 方式一:
        int ageSum1 = students.stream().mapToInt(Student::getAge).sum();
        // 70
        System.out.println("年龄总和" + ageSum1);
        // 方式二:
        int ageSum2 = students.stream().map(Student::getAge).reduce(Integer::sum).orElse(0);
        // 70
        System.out.println("年龄总和" + ageSum2);
    }

    private List<Student> buildTestData() {
        return new ArrayList<Student>() {{
            add(new Student() {{
                setName("A");
                setAge(10);
            }});
            add(new Student() {{
                setName("B");
                setAge(10);
            }});
            add(new Student() {{
                setName("C");
                setAge(10);
            }});
            add(new Student() {{
                setName("D");
                setAge(10);
            }});
            add(new Student() {{
                setName("E");
                setAge(10);
            }});
            add(new Student() {{
                setName("F");
                setAge(10);
            }});
            add(new Student() {{
                setName("G");
                setAge(10);
            }});
        }};

    }
}

2019/05/11--关于Mybatis的几个问答整理

关于 Mybatis 的几个问题

1. #{} 和 ${} 的区别是什么?
答: #{} 是预编译处理,${} 是字符串替换。
mybatis 在处理 #{} 时,会将 sql 中的 #{} 替换为 ? 号,调用 PreparedStatement 的 set 方法来赋值;mybatis 在处理 ${} 时,就是把 ${} 替换成变量的值。
#{} 能够很大程度上防止 sql 注入;${} 方式无法防止 sql 注入。
${} 一般用于传入数据库对象,比如数据库表名。
能用 #{} 时尽量用 #{}。
mybatis 排序时使用 order by 动态参数时需要注意,使用 ${} 而不用 #{}。

2. 通常一个Xml映射文件,都会写一个Dao接口与之对应,请问,这个Dao接口的工作原理是什么?Dao接口里的方法,参数不同时,方法能重载吗?
答: Dao接口,就是人们常说的Mapper接口,接口的全限名,就是映射文件中的namespace的值,接口的方法名,就是映射文件中MappedStatement的id值,接口方法内的参数,就是传递给sql的参数。Mapper接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符串作为key值,可唯一定位一个MappedStatement,举例:com.mybatis3.mappers.StudentDao.findStudentById,可以唯一找到namespace为com.mybatis3.mappers.StudentDao下面id = findStudentById的MappedStatement。在Mybatis中,每一个<select><insert><update><delete>标签,都会被解析为一个MappedStatement对象。
Dao接口里的方法,是不能重载的,因为是全限名+方法名的保存和寻找策略。
sql语句的名称是由:Mapper 接口的名称与对应的方法名称组成的。
Dao接口的工作原理是JDK动态代理,Mybatis运行时会使用JDK动态代理为Dao接口生成代理proxy对象,代理对象proxy会拦截接口方法,转而执行MappedStatement所代表的sql,然后将sql执行结果返回。

3. Mybatis是如何进行分页的?分页插件的原理是什么?
答: Mybatis使用RowBounds对象进行分页,它是针对ResultSet结果集执行的内存分页,而非物理分页,可以在sql内直接书写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物理分页。
分页插件的基本原理是使用Mybatis提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql,然后重写sql,根据dialect方言,添加对应的物理分页语句和物理分页参数。
举例:select * from student,拦截sql后重写为:select t.* from (select * from student)t limit 0,10;

4. Mybatis是如何将sql执行结果封装为目标对象并返回的?都有哪些映射形式?
答: 第一种是使用 <resultMap> 标签,逐一定义列名和对象属性名之间的映射关系。第二种使用 sql 列的别名功能,将列别名写为对象属性名,比如T_NAME AS NAME,对象属性名一般是 name,小写,但是列名不区分大小写,Mybatis 忽略列名大小写,智能找到与之对应对象属性名,你甚至可以写成 T_NAME AS NaMe,Mybatis 一样可以正常工作。有了列名与属性名的映射关系后,Mybatis 通过反射创建对象,同时使用反射给对象属性逐一赋值并返回,那些找不到映射关系的属性,是无法完成赋值的。

5. Xml映射文件中,除了常见的select|insert|update|delete标签之外,还有哪些标签?
答: 还有很多其他的标签,加上动态sql的9个标签,trim|where|set|foreach|if|choose|when|otherwise|bind等,其中为sql片段标签,通过标签引入sql片段,为不支持自增的主键生成策略标签。

6. 简述Mybatis 的插件运行原理,以及如何编写一个插件?
答: Mybatis仅可以编写针对ParameterHandler、ResultSetHandler、StatementHandler、Executor这4种接口的插件,Mybatis使用JDK的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这4种接口对象的方法时,就会进入拦截方法,具体就是InvocationHandler的invoke()方法,当然,只会拦截那些你指定需要拦截的方法。实现Mybatis的Interceptor接口并复写intercept()方法,然后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可,记住,还需要在配置文件中配置你编写的插件。

7. 一级、二级缓存
答: 一级缓存:基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session,当 Session flush 或 close 之后,该 Session 中的所有 Cache 就将清空。
二级缓存:与一级缓存机制相同,默认也是采用 PerpetualCache,HashMap 存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源,如Ehcache。要开启二级缓存,你需要在你的 SQL 映射文件中添加一行:<cache/>
对于缓存数据的更新机制,当某一个作用域(一级缓存 Session / 二级缓存 Namespace)进行了 C/U/D 操作后,默认该作用域下所有 select 中的缓存将被 clear。

8. Mybatis 是否支持延迟加载?如果支持,它的实现原理是什么?
答: Mybatis 仅支持 association 关联对象和 collection 关联集合对象的延迟加载,association 指的就是一对一,collection 指的就是一对多查询。在 Mybatis 配置文件中,可以配置是否启用延迟加载 lazyLoadingEnable=true|false
它的原理是,使用 CGLIB 创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用 a.getB().getName(),拦截器 invoke() 方法发现 a.getB() 是 null 值,那么就会单独发送事先保存好的查询关联 B 对象的 sql,把 B 查询上来,然后调用 a.setB(b),于是 a 的对象 b 属性就有值了,接着完成 a.getB().getName() 方法的调用。这就是延迟加载的原理。

9. Mybatis映射文件中,如果A标签通过include引用了B标签的内容,请问,B标签能否定义在A标签的后面,还是说必须定义在A标签的前面?
答: 虽然Mybatis解析Xml映射文件是按照顺序解析的,但是,被引用的B标签依然可以定义在任何地方,Mybatis都可以正确识别。
原理是,Mybatis解析A标签,发现A标签引用了B标签,但是B标签尚未解析到,尚不存在,此时,Mybatis会将A标签标记为未解析状态,然后继续解析余下的标签,包含B标签,待所有标签解析完毕,Mybatis会重新解析那些被标记为未解析的标签,此时再解析A标签时,B标签已经存在,A标签也就可以正常解析完成了。

10. 简述 Mybatis 的 XML 映射文件和 Mybatis 内部数据结构之间的映射关系?
答: Mybatis 将所有Xml配置信息都封装到 All-In-One 重量级对象 Configuration 内部。在Xml映射文件中,<parameterMap>标签会被解析为 ParameterMap 对象,其每个子元素会被解析为 ParameterMapping 对象。<resultMap>标签会被解析为 ResultMap 对象,其每个子元素会被解析为 ResultMapping 对象。每一个 <select><insert><update><delete>标签均会被解析为 MappedStatement 对象,标签内的 sql 会被解析为 BoundSql 对象。

2019/07/09--List<Integer>通过 Stream 求和

        List<Integer> intList = new ArrayList<Integer>() {{
            add(1);
            add(2);
            add(3);
            add(4);
        }};

        Integer sum = intList.parallelStream().reduce(Integer::sum).orElse(0);
        System.out.println(sum);

2019/04/12--谈谈`@Async`注解以及`Future`类型

谈谈@Async注解以及Future类型

在项目中看到使用了 @AsyncFuture ,一眼看上去有点陌生,于是便简单了解一下,下面就简要谈一下。事先说明,项目框架为 spring-boot,所以前提是spring-boot项目。

@Async 实现异步调用

“异步调用”对应的是“同步调用”,同步调用指程序按照定义顺序依次执行,每一行程序都必须等待上一行程序执行完成之后才能执行;异步调用指程序在顺序执行时,不等待异步调用的语句返回结果就执行后面的程序。

顾名思义,@Async是用来实现异步的。基于@Async的方法,称之为异步方法。这些方法将在执行的时候,将会在独立的线程中被执行,调用者无需等待它的完成,即可继续其他的操作。

那么在Spring中如何使用@Async实现异步调用呢?

假如我们有一个Task类,其中有三个任务需要异步执行,那么我们就可以将这些任务方法标上@Async注解,使其成为异步方法。代码如下:

@Component
public class AsyncTask {
    private static Random random = new Random();

    @Async
    public void doTaskOne() throws Exception {
        System.out.println("开始做任务一");
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        System.out.println("完成任务一,耗时:" + (end - start) + "毫秒");
    }

    @Async
    public void doTaskTwo() throws Exception {
        System.out.println("开始做任务二");
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        System.out.println("完成任务二,耗时:" + (end - start) + "毫秒");
    }

    @Async
    public void doTaskThree() throws Exception {
        System.out.println("开始做任务三");
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        System.out.println("完成任务三,耗时:" + (end - start) + "毫秒");
    }
}

为了让@async注解能够生效,还需要在Spring Boot的主程序中配置@EnableAsync,如下所示:

@SpringBootApplication
@EnableAsync
public class Application {
  public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
  }
}

然后我们可以写一个单元测试进行测试一下:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
public class ApplicationTests {
    @Autowired
    private Task task;
	
    @Test
    public void test() throws Exception {
	task.doTaskOne();
	task.doTaskTwo();
	task.doTaskThree();
    }
}

此时可以反复执行单元测试,您可能会遇到各种不同的结果,比如:

  • 没有任何任务相关的输出
  • 有部分任务相关的输出
  • 乱序的任务相关的输出

原因是目前doTaskOne、doTaskTwo、doTaskThree三个函数的时候已经是异步执行了。主程序在异步调用之后,主程序并不会理会这三个函数是否执行完成了,由于没有其他需要执行的内容,所以程序就自动结束了,导致了不完整或是没有输出任务相关内容的情况。

注:@async所修饰的函数不要定义为static类型,这样异步调用不会生效。

Funture 类型

那么问题来了,什么是Future类型呢?

Future是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果的接口。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。

它的接口定义如下:

public interface Future<V> {
    boolean cancel(boolean mayInterruptIfRunning);
    boolean isCancelled();
    boolean isDone();
    V get() throws InterruptedException, ExecutionException;
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

它声明这样的五个方法:

  • cancel方法用来取消任务,如果取消任务成功则返回true,如果取消任务失败则返回false。参数mayInterruptIfRunning表示是否允许取消正在执行却没有执行完毕的任务,如果设置true,则表示可以取消正在执行过程中的任务。如果任务已经完成,则无论mayInterruptIfRunning为true还是false,此方法肯定返回false,即如果取消已经完成的任务会返回false;如果任务正在执行,若mayInterruptIfRunning设置为true,则返回true,若mayInterruptIfRunning设置为false,则返回false;如果任务还没有执行,则无论mayInterruptIfRunning为true还是false,肯定返回true。
  • isCancelled方法表示任务是否被取消成功,如果在任务正常完成前被取消成功,则返回 true。
  • isDone方法表示任务是否已经完成,若任务完成,则返回true;
  • get()方法用来获取执行结果,这个方法会产生阻塞,会一直等到任务执行完毕才返回;
  • get(long timeout, TimeUnit unit)用来获取执行结果,如果在指定时间内,还没获取到结果,就直接返回null。

也就是说Future提供了三种功能:

  • 判断任务是否完成;
  • 能够中断任务;
  • 能够获取任务执行结果。

对于上面的Task类,我们也可以简单修改一下,判断上述三个异步调用是否已经执行完成。类似于如下doTaskOne()代码进行修改:

    @Async
    public Future<String> doTaskOne() throws Exception {
        System.out.println("开始做任务一");
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        System.out.println("完成任务一,耗时:" + (end - start) + "毫秒");
        return new AsyncResult<>("任务一完成");
    }

然后我们再修改一下单元测试方法:

    @Test
    public void asyncTaskTest() throws Exception {
        long start = System.currentTimeMillis();

        Future<String> task1 = asyncTask.doTaskOne();
        Future<String> task2 = asyncTask.doTaskTwo();
        Future<String> task3 = asyncTask.doTaskThree();

        // 三个任务都调用完成,退出循环等待
        while (!task1.isDone() || !task2.isDone() || !task3.isDone()) {
            Thread.sleep(1000);
        }

        long end = System.currentTimeMillis();
        System.out.println("任务全部完成,总耗时:" + (end - start) + "毫秒");
    }

输出结果如下:

开始做任务一
开始做任务二
开始做任务三
完成任务二,耗时:5352毫秒
完成任务一,耗时:7190毫秒
完成任务三,耗时:7525毫秒
任务全部完成,总耗时:8004毫秒

总结

如果在业务场景中我们有异步以及需要知道异步方法执行结果的需求,那么@Async以及Future的组合会是个不错的选择。另外 Java 8 中添加了新类 CompletableFuture 感兴趣的同学也可以自行进行更多的尝试。

链接:

2017/12/10--翻译:深度学习,机器学习以及AI之间的不同

原文地址:https://www.forbes.com/sites/bernardmarr/2016/12/08/what-is-the-difference-between-deep-learning-machine-learning-and-ai/#7efff20626cf

在过去的几年里,当有了关于人工智能(AI),大数据以及分析学的讨论之后,术语“深度学习”已然进入了商业语言的范畴。而且有充分的理由——这是通往AI的道路,而人工智能在开发使许多行业发生革命性变化的自主自学系统方面显现出巨大的前景。

谷歌的语音识别和图像识别算法,Netflix 和亚马逊针对你接下来想看的或者想买的决策上,以及MIT的研究员预测未来上,都用到了深度学习。一直以来,销售这些工具的不断增长的行业总是热衷于谈论其如何带来的革命。

在上一篇文章中我写了和关于AI和机器学习的不同之处。而机器学习经常被认为是人工智能的子学科,比较好的观点是,把它作为人工智能目前最先进的一个领域,如今正在为各个行业以及社会可以用来驱动变革的工具展示着最大的潜能。

那么接下来,可能对理解深度学习是前沿(技术)的前沿非常有帮助。ML涉及到几个人工智能核心的创意并集中于如何用这些创意,通过模仿人类自己决策的神经网络来解决现实世界中的问题。深度学习则是对机器学习工具及技术的较小子范畴更为专注,并把它们应用于解决一切需要“思考”的问题——无论是人类去思考还是人工去思考。

它是怎么工作的?

实质上深度学习意味着要给计算机系统“补给”大量数据,用以对其他数据做出决策。这类数据通过神经网络提供,就像机器学习的案例一样。这些逻辑网络对每一位通过的数据或问一系列的是非问题,或提取数字值,然后根据得到的答案进行分类。
Shutterstock

因为深度学习专注于发展这些网络,它们便以深度神经网络(Deep Neural Networks)为人所知,逻辑网络的复杂性在于需要解决对大量级数据集的处理,比如谷歌的图片库,或者推特的推文流水量。

类似于此巨大的数据集,以及足够复杂的逻辑网络来解决它们的分类问题,故对于计算机来说,将其表示的图像和状态以很高的准确率呈现给人类是微不足道的。

图片是来说明工作原理的很好的例子,因为它们包含很多不同的元素,并且掌握一台计算机如何用它的单轨、专注计算的内核学习像人类那样理解这些图片,对我们来说并非易事。但是深度学习可以应用于任何数据格式——机器信号,音频,视频,口语,书面语——并且非常非常快地得出和人类相似的结论。让我们看一个实际的例子。

一个系统用以自动记录和报告某个特定车型的汽车在某条公路上有多少辆驶过。首先,会给它提供访问某个庞大的汽车类型数据库的权限,包括它们的外形,尺寸,甚至是引擎的声音。这(数据)可以手动编写,或者更为高级的情况,如果系统被编程为搜索互联网,那么就能自动收集,并提取其找到的数据。

接下来它会取一些需要处理的数据——真实世界的数据,其中包含各种视角,在这种情况下就要通过路边摄像头和麦克风来计算。通过对比从它的传感器得到数据和“学习”获得的数据,它可以根据驶过车辆的牌子和型号进行分类,而且具有一定准确性。

到目前为止都是非常简单的。“深度”在何处体现呢?系统随着时间的推移,它会获取更多经验,可以提升准确分类的概率,通过得到的新数据来“训练”自己。换句话说,它也可以像我们一样从错误中学习。比如,它不能根据一些车辆相似的外形和引擎声正确地判断它们的牌子和型号,忽略了另一个差异因素,它认为对决策的重要性很低。通过了解这个区别对于理解两辆车之间的差异是至关重要的,这会提高下一次得到正确结果的可能性。

那么深度学习能做什么呢?

可能结束这篇文章最好的方式便是给出为何有如此多的突破,然后给出更多的在当今如何使用深度学习的例子。一些优秀的应用正在投入使用或者正在工作,包括:

自动驾驶汽车的导航。利用传感器和板载分析,汽车适当地运用机器学习来学习识别障碍并对其作出反应。

给图像褪色。通过让计算机分辨物体以及了解它们在人类眼中的样子,图片和视频可以被计算机转成黑白的。

预测法律程序的结果。一个由英美研究人员开发的系统最近被证明,当为其提供案子的一些基本事实之后,该系统可以正确地预测法庭的判决。

精准医学。深度学习正被用于开发适合个体基因组的药物。

自动分析报告。系统可以分析数据并且用人类的自然语言提供意见报告,并提供我们可以轻易理解的图表。

打游戏。深度学习系统曾被教授打(然后赢得)游戏,比如棋盘游戏GO,和Atari的电子游戏Breakout

当讨论(尤其是,出售)这些前沿技术的时候,很容易因为经常使用的炒作和夸张而得意忘形。但事实上,都是应该的。因为听到数据科学家谈论他们拥有的工具和技术并不常见,他们也不希望这么快出现看到这一景象,并且很多都是归功于机器学习和深度学习所取得的进步。

2018/07/19--FieldUtils的用法

获取对象属性的方法:

obj.getClass().getFields()怎么是无效的?

FieldUtils.getAllFields(obj.getClass())是怎么实现的,以及能不能只获取到一部分field而不去获取两个final的class(this.class && obj.this.class)? FieldUtils的其他用法。

FieldUtils的简单介绍

2019/09/29--Map*操作之 `merge()` 方法

Java 8 中 Map *操作之 merge() 的用法

Java 8 最大的特性无异于更多地面向函数,比如引入了 lambda等,可以更好地进行函数式编程。前段时间无意间发现了 map.merge() 方法,感觉还是很好用的,此文简单做一些相关介绍。首先我们先看一个例子。

merge() 怎么用?

假设我们有这么一段业务逻辑,我有一个学生成绩对象的列表,对象包含学生姓名、科目、科目分数三个属性,要求求得每个学生的总成绩。加入列表如下:

    private List<StudentScore> buildATestList() {
        List<StudentScore> studentScoreList = new ArrayList<>();
        StudentScore studentScore1 = new StudentScore() {{
            setStuName("张三");
            setSubject("语文");
            setScore(70);
        }};
        StudentScore studentScore2 = new StudentScore() {{
            setStuName("张三");
            setSubject("数学");
            setScore(80);
        }};
        StudentScore studentScore3 = new StudentScore() {{
            setStuName("张三");
            setSubject("英语");
            setScore(65);
        }};
        StudentScore studentScore4 = new StudentScore() {{
            setStuName("李四");
            setSubject("语文");
            setScore(68);
        }};
        StudentScore studentScore5 = new StudentScore() {{
            setStuName("李四");
            setSubject("数学");
            setScore(70);
        }};
        StudentScore studentScore6 = new StudentScore() {{
            setStuName("李四");
            setSubject("英语");
            setScore(90);
        }};
        StudentScore studentScore7 = new StudentScore() {{
            setStuName("王五");
            setSubject("语文");
            setScore(80);
        }};
        StudentScore studentScore8 = new StudentScore() {{
            setStuName("王五");
            setSubject("数学");
            setScore(85);
        }};
        StudentScore studentScore9 = new StudentScore() {{
            setStuName("王五");
            setSubject("英语");
            setScore(70);
        }};

        studentScoreList.add(studentScore1);
        studentScoreList.add(studentScore2);
        studentScoreList.add(studentScore3);
        studentScoreList.add(studentScore4);
        studentScoreList.add(studentScore5);
        studentScoreList.add(studentScore6);
        studentScoreList.add(studentScore7);
        studentScoreList.add(studentScore8);
        studentScoreList.add(studentScore9);

        return studentScoreList;
    }

我们先看一下常规做法:

        ObjectMapper objectMapper = new ObjectMapper();
        List<StudentScore> studentScoreList = buildATestList();

        Map<String, Integer> studentScoreMap = new HashMap<>();
        studentScoreList.forEach(studentScore -> {
            if (studentScoreMap.containsKey(studentScore.getStuName())) {
                studentScoreMap.put(studentScore.getStuName(), 
                                    studentScoreMap.get(studentScore.getStuName()) + studentScore.getScore());
            } else {
                studentScoreMap.put(studentScore.getStuName(), studentScore.getScore());
            }
        });

        System.out.println(objectMapper.writeValueAsString(studentScoreMap));

// 结果如下:
// {"李四":228,"张三":215,"王五":235}

然后再看一下 merge() 是怎么做的:

        Map<String, Integer> studentScoreMap2 = new HashMap<>();
        studentScoreList.forEach(studentScore -> studentScoreMap2.merge(
          studentScore.getStuName(),
          studentScore.getScore(),
          Integer::sum));

        System.out.println(objectMapper.writeValueAsString(studentScoreMap2));

// 结果如下:
// {"李四":228,"张三":215,"王五":235}

merge() 简介

merge() 可以这么理解:它将新的值赋值到 key (如果不存在)或更新给定的key 值对应的 value,其源码如下:

    default V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
        Objects.requireNonNull(remappingFunction);
        Objects.requireNonNull(value);
        V oldValue = this.get(key);
        V newValue = oldValue == null ? value : remappingFunction.apply(oldValue, value);
        if (newValue == null) {
            this.remove(key);
        } else {
            this.put(key, newValue);
        }

        return newValue;
    }

我们可以看到原理也是很简单的,该方法接收三个参数,一个 key 值,一个 value,一个 remappingFunction ,如果给定的key不存在,它就变成了 put(key, value) 。但是,如果 key 已经存在一些值,我们 remappingFunction 可以选择合并的方式,然后将合并得到的 newValue 赋值给原先的 key。

使用场景

这个使用场景相对来说还是比较多的,比如分组求和这类的操作,虽然 stream 中有相关 groupingBy() 方法,但如果你想在循环中做一些其他操作的时候,merge() 还是一个挺不错的选择的。

其他

除了 merge() 方法之外,我还看到了一些Java 8 中 map 相关的其他方法,比如 putIfAbsentcompute()computeIfAbsent()computeIfPresent,这些方法我们看名字应该就知道是什么意思了,故此处就不做过多介绍了,感兴趣的可以简单阅读一下源码(都还是挺易懂的),这里我们贴一下 compute()(Map.class) 的源码,其返回值是计算后得到的新值:

    default V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
        Objects.requireNonNull(remappingFunction);
        V oldValue = this.get(key);
        V newValue = remappingFunction.apply(key, oldValue);
        if (newValue == null) {
            if (oldValue == null && !this.containsKey(key)) {
                return null;
            } else {
                this.remove(key);
                return null;
            }
        } else {
            this.put(key, newValue);
            return newValue;
        }
    }

总结

本文简单介绍了一下 Map.merge() 的方法,除此之外,Java 8 中的 HashMap 实现方法使用了 TreeNode 和 红黑树,在源码阅读上可能有一点难度,不过原理上还是相似的,compute() 同理。所以,源码肯定是要看的,不懂的地方多读多练自然就理解了。

链接

2018/12/24--.editorconfig的作用

EditorConfig 是什么?

顾名思义,EditorConfig就是编辑器配置,帮助开发人员在不同的编辑器和IDE之间定义和维护一致的编码样式,由用于定义编码样式的文件格式和一组文本编辑器插件组成,这些插件使编辑器能够读取文件格式并遵循定义的样式。EditorConfig文件易于阅读,并且与版本控制系统配合使用。

.editorconfig示例

下面是一个.editorconfig文件的示例,为Python和Javascript文件设置了行尾以及缩进的样式。

# EditorConfig is awesome: https://EditorConfig.org

# top-most EditorConfig file
root = true

# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true

# Matches multiple files with brace expansion notation
# Set default charset
[*.{js,py}]
charset = utf-8

# 4 space indentation
[*.py]
indent_style = space
indent_size = 4

# Tab indentation (no size specified)
[Makefile]
indent_style = tab

# Indentation override for all JS under lib directory
[lib/**.js]
indent_style = space
indent_size = 2

# Matches the exact files either package.json or .travis.yml
[{package.json,.travis.yml}]
indent_style = space
indent_size = 2

当用IDE打开一个文件时,EditorConfig插件会在打开文件的目录和其每一级父节点查找.editorconfig文件,直到找到一个配置了root = true的配置文件。

文件格式详情

EditorConfig文件使用INI格式。斜杠(/)作为路径分隔符,#或者;作为注释。路径支持通配符:

通配符 说明
* 匹配除/之外的任意字符
** 匹配任意字符串
? 匹配任意单个字符
[name] 匹配name字符
[!name] 不匹配name字符
[s1,s2,s3] 匹配给定的字符串
[num1..num2] 匹配num1到mun2直接的整数

EditorConfig支持以下属性:

属性 说明
indent_style 缩进使用tab或者space
indent_size 缩进为space时,缩进的字符数
tab_width 缩进为tab时,缩进的宽度
end_of_line 换行符的类型。lf, cr, crlf三种
charset 文件的charset。有以下几种类型:latin1, utf-8, utf-8-bom, utf-16be, utf-16le
trim_trailing_whitespace 是否将行尾空格自动删除
insert_final_newline 是否使文件以一个空白行结尾
root 表明是最顶层的配置文件,发现设为true时,才会停止查找.editorconfig文件

支持的编辑器及IDE

无需安装插件的

这些编辑器捆绑了对EditorConfig的原生支持。
_20181224144529

需要安装插件的

要将EditorConfig与其中一个编辑器一起使用,需要安装一个插件。
_20181224145127

要将EditorConfig与其中一个无头工具一起使用,也需要安装一个插件。
_20181224145019

总结:可以解决哪些问题?

  1. 解决markdown文件行尾空格自动删除的问题
# http://editorconfig.org
root = true

[*]
trim_trailing_whitespace = true

[*.md]
trim_trailing_whitespace = false
  1. 解决github代码展示或者是团队开发项目格式不统一的问题
# editorconfig.org
root = true

[*]
indent_size = 2
indent_style = space
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

更多

官网:https://editorconfig.org/
wiki文档:https://github.com/editorconfig/editorconfig/wiki/EditorConfig-Properties

2018/01/03--java 8中List<Object>根据Object属性去重代码

import lombok.Data;
import org.junit.Test;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.TreeSet;

import static java.util.stream.Collectors.collectingAndThen;
import static java.util.stream.Collectors.toCollection;

/**
 * @author liuqian
 */
public class RemoveDuplicates {

  @Test
  public void removeDuplicatesByName() {
    Person person1 = new Person() {{
      setId(1);
      setName("person1");
    }};
    Person person2 = new Person() {{
      setId(2);
      setName("person2");
    }};
    Person person3 = new Person() {{
      setId(3);
      setName("person2");
    }};

    List<Person> personList = new ArrayList<>();
    personList.add(person1);
    personList.add(person2);
    personList.add(person3);
    System.out.println("====================before================");
    for (Person person : personList) {
      System.out.println(person.toString());
    }
    List<Person> unique = personList.stream().collect(
      collectingAndThen(
        toCollection(() -> new TreeSet<>(Comparator.comparing(Person::getName))), ArrayList::new)
    );
    System.out.println("------------------after-------------------");
    for (Person person : unique) {
      System.out.println(person.toString());
    }
  }

  @Data
  private class Person {
    private int id;
    private String name;

    @Override
    public String toString() {
      return "Person: id is " + id + ", name is " + name;
    }
  }
}

输出结果:

====================before================
Person: id is 1, name is person1
Person: id is 2, name is person2
Person: id is 3, name is person2
------------------after-------------------
Person: id is 1, name is person1
Person: id is 2, name is person2

2018/01/16--Elasticsearch报错出现乱码

参考:https://elasticsearch.cn/question/974
_20180116165932

将elasticsearch 目录下的 config/jvm.options 文件里把“-Dfile.encoding=UTF-8”改为“-Dfile.encoding=GBK”
然后重启 Elasticsearch 即可,修改之后报错原因为:

[2018-01-16T16:55:18,564][WARN ][o.e.t.n.Netty4Transport  ] [node-1] exception caught on transport layer [org.elasticsearch.transport.netty4.NettyTcpChannel@1ff4527f], closing connection
java.io.IOException: 远程主机强迫关闭了一个现有的连接。

2019/03/08--MySQL中的事务以及锁的介绍

MySQL事务详解

MySQL 事务主要用于处理操作量大,复杂度高的数据。比如说,在人员管理系统中,你删除一个人员,你即需要删除人员的基本资料,也要删除和该人员相关的信息,如信箱,文章等等,这样,这些数据库操作语句就构成一个事务!

  • 在 MySQL 中只有使用了 Innodb 数据库引擎的数据库或表才支持事务。
  • 事务处理可以用来维护数据库的完整性,保证成批的 SQL 要么全部执行,要么全部不执行。
  • 事务用来管理 insert,update,delete 语句

四大特性(ACID)

一般来说,事务必须满足4个条件。

  • 原子性(Atomicity) :事务开始后所有操作,要么全部做完,要么全部不做,不会结束在中间环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态。
  • 一致性(Consistency) :在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发地完成预定的工作。
  • 隔离性(Isolation) :数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致的数据不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(serializable)。
  • 持久性(Durability) :事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

在 MySQL 命令行的默认设置下,事务都是自动提交的,即执行 SQL 语句后就会马上执行 COMMIT 操作。因此要显式地开启一个事务务须使用命令 BEGIN 或 START TRANSACTION,或者执行命令 SET AUTOCOMMIT=0,用来禁止使用当前会话的自动提交。

事务控制语句

  • BEGIN 或 START TRANSACTION 显式地开启一个事务;
  • COMMIT 也可以使用 COMMIT WORK,不过二者是等价的。COMMIT 会提交事务,并使已对数据库进行的所有修改成为永久性的;
  • ROLLBACK 也可以使用 ROLLBACK WORK,不过二者是等价的。回滚会结束用户的事务,并撤销正在进行的所有未提交的修改;
  • SAVEPOINT identifier,SAVEPOINT 允许在事务中创建一个保存点,一个事务中可以有多个 SAVEPOINT;
  • RELEASE SAVEPOINT identifier 删除一个事务的保存点,当没有指定的保存点时,执行该语句会抛出一个异常;
  • ROLLBACK TO identifier 把事务回滚到标记点;
  • SET TRANSACTION 用来设置事务的隔离级别。InnoDB 存储引擎提供事务的隔离级别有READ UNCOMMITTED、READ COMMITTED、REPEATABLE READ 和 SERIALIZABLE。

MySQL事务处理主要有两种方法

1、用 BEGIN, ROLLBACK, COMMIT来实现

  • BEGIN 开始一个事务
  • ROLLBACK 事务回滚
  • COMMIT 事务确认

2、直接用 SET 来改变 MySQL 的自动提交模式:

  • SET AUTOCOMMIT=0 禁止自动提交
  • SET AUTOCOMMIT=1 开启自动提交

事务的并发问题

  • 脏读 :事务 A 读取事务 B 更新的数据,然后 B 执行回滚操作,那么A读取到的数据为脏数据。
  • 不可重复读 : 事务 A 多次读取同一数据,事务 B 在事务 A 读取的过程中,对数据作了更改,导致事务 A 多次读取同一事务时,结果不一致。
  • 幻读 :系统管理员A将数据库中所有学生成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。

小结:不可重复读和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表。

MySQL事务隔离级别

事务隔离级别 脏读 不可重复读 幻读
读未提交(read uncommitted)
读提交(read committed)
可重复读(repeatable read)
串行化(serilizable)

注:MySQL默认的事务隔离级别为可重复读(repeatable read)。

补充:

  • 事务隔离级别为读提交时,写数据只会写入相应的行。
  • 事务隔离级别为可重复读时,如果检索条件有索引(包括主键索引)的时候,默认加锁方式是next-key锁;如果检索条件没有索引,更新数据时会整张表。一个间隙被事务加了锁,其他事务是不能在这个间隙插入记录的,这样可以防止幻读。
  • 事务隔离级别为串行化时,读写数据都会锁住整张表。
  • 隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。

MySQL中的锁

概述

MySQL锁的类型分为共享锁(又称读锁)、排他锁(又称写锁),此外还有悲观锁和乐观锁,以及表(级)锁、行(级)锁。

InnoDB引擎的锁机制:InnoDB支持事务,支持行锁和表锁用的比较多,Myisam不支持事务,只支持表锁。

共享锁(读锁)、排他锁(写锁)

  • 共享锁(S):允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。
  • 排他锁(X):允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和排他写锁。
  • 意向共享锁(IS):事务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁。
  • 意向排他锁(IX):事务打算给数据行加行排他锁,事务在给一个数据行加排他锁前必须先取得该表的IX锁。

说明:

  1. 共享锁和排他锁都是行锁,意向锁都是表锁,应用中我们只会用到共享锁和排他锁,意向锁是MySQL内部使用的,不需要用户干预。
  2. 对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据加排他锁(X);对于普通SELECT语句,InnoDB不会加任何锁,事务可以通过以下语句显式给记录集加共享锁或排他锁:
// 共享锁
SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE;

// 排他锁
SELECT * FROM table_name WHERE ... FOR UPDATE;

对于锁定行记录后需要进行更新操作的应用,应该使用Select...For update 方式,获取排它锁。(用共享锁,在读了之后再写会阻塞,会导致死锁)

这里说说Myisam:MyISAM在执行查询语句(SELECT)前,会自动给涉及的所有表加读锁,在执行更新操作(UPDATE、DELETE、INSERT等)前,会自动给涉及的表加写锁。

  1. InnoDB行锁是通过给索引上的索引项加锁来实现的,因此InnoDB这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁!

乐观锁、悲观锁

悲观锁
正如其名,指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。

  1. 使用悲观锁,我们必须关闭mysql数据库的自动提交属性,采用手动提交事务的方式,因为MySQL默认使用autocommit模式,也就是说,当你执行一个更新操作后,MySQL会立刻将结果进行提交。
  2. 需要注意的是,在事务中,只有 SELECT ... FOR UPDATELOCK IN SHARE MODE 同一笔数据时会等待其它事务结束后才执行,一般SELECT ... 则不受此影响。对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁(X)。
  3. 补充:MySQL select…for update的Row Lock与Table Lock
    使用select…for update会把数据给锁住,不过我们需要注意一些锁的级别,MySQL InnoDB默认Row-Level Lock,所以只有「明确」地指定主键(或有索引的地方),MySQL 才会执行Row lock (只锁住被选取的数据) ,否则MySQL 将会执行Table Lock (将整个数据表单给锁住)。

乐观锁
相对于悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会对数据的冲突与否进行检测,如果发现冲突了,则返回用户错误的信息,让用决定如何去做(一般是回滚事务)。实现乐观锁一般有以下两种方式:

  1. 使用数据版本(Version)记录机制实现,这是乐观锁最常用的一种实现方式。何谓数据版本?即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。
  2. 乐观锁定的第二种实现方式和第一种差不多,同样是在需要乐观锁控制的table中增加一个字段,名称无所谓,字段类型使用时间戳(timestamp), 和上面的version类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突。

总结:两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行retry,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。另外,高并发情况下个人认为乐观锁要好于悲观锁,因为悲观锁的机制使得各个线程等待时间过长,极其影响效率,乐观锁可以在一定程度上提高并发度。

表锁、行锁

表级锁(table-level locking):MyISAM和MEMORY存储引擎
行级锁(row-level locking) :InnoDB存储引擎
页面锁(page-level-locking):BDB存储引擎

表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。
行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。

2018/01/18--mysql查询语句合并查询结果

在使用查询语句时,有时候需要用到关联查询,查询结果会出现group by一列出现对应数行的情况。
sql如下:
select * from table group by 列字段;
合并查询出的列:
select GROUP_CONCAT(查询的字段 separator ';') from table group by 列字段;
其中GROUP_CONCAT()方法可以直接使用GROUP_CONCAT(cloum)默认以","相隔,亦可以自定义分割符,如GROUP_CONCAT(cloum separator ';')

2019/01/26--我发布了一个毫无意义的npm包

我发布了一个毫无意义的npm包

最近几个月一直在做前端的项目,然后在安装依赖的时候便打出命令 npm install xxx 来进行局部或者全局安装就行了,甚至在包含package.json文件的项目中执行 npm ci 就自动引入依赖了。嗯,我就在想,我可以发布npm包吗?接着,便进行了一次毫无意义的尝试,仅作记录。

前期调研

那么首先就是,怎么发布一个npm包呢?

步骤如下:

  • 第一步,注册一个npm账号,免费的
  • 第二步,新建一个项目,并通过 npm init 进行项目的初始化配置
  • 第三步,进入项目目录,执行 npm publish 便可以发布到 npm 公共仓库进行下载了(网站地址:https://www.npmjs.com/)
    注:在操作过程中可能遇到各种各样的小问题,不过大致看一眼报错日志就能解决了,简单步骤就这三步。
    下面具体介绍。

创建一个无意义的项目

既然没有什么意义,那趣味性一定要有,于是便参考了github上的一个项目:five ,我创建一个项目 two

接着便是完善这个项目了。除此之外,我搜了一下npm,发现“two”这个名字已经有人取了,便不得不再想一个,否则发布便会失败,然后就加了一个前缀“monkey-two”。

初始化项目

  • 创建项目目录 ./monkey-two
  • 进入项目目录,执行 npm init
    _20190126204230
    然后一步步执行就行了,最终会自动帮我们生成一个package.json文件,敲入yes或者直接回车就行了。
    _20190126204648

完善项目

  • 创建index.js之类的文件,在此文件中引入 two.js ,然后所有的主要函数和实现都在此文件中完成
  • 编写测试,一个优(wu)秀(liao)的项目怎么能没有测试呢?添加 test.js 文件,本项目的测试是用 mocha 来进行的
  • 编写README.md,这个文件主要是说明该项目的使用方法以及适用场景,而且特别特别重要的是,一定要吸睛,这一点我还没做好,今后会完善的,比如加上各种标签、增加色彩和图片什么的

这项目能干啥

Basic 2(基础数字2)
two(); // 2
two.valueOf(); // 2
Addition(加法)
two() + two(); // 2 + 2 = 4
two.add(1); // 1 + 2 = 3
two.add(2); // 2 + 2 = 4
two.add(3); // 3 + 2 = 5
two.add(10, 5); // 10 + 5 = 15
Subtraction(减法)
two() - two(); // 2 - 2 = 0
two.subtract(1); // 1 - 2 = -1
two.subtract(2); // 2 - 2 = 0
two.subtract(3); // 3 - 2 = 1
two.subtract(10, 5); // 10 - 5 = 5
Multiplication(乘法)
two() * two(); // 2 * 2 = 4
two.times(1); // 1 * 2 = 2
two.times(2); // 2 * 2 = 4
two.times(3); // 3 * 2 = 6
two.times(10, 5); // 10 * 5 = 50
Multiplication(除法)
two() / two(); // 2 / 2 = 1
two.divide(1); // 1 / 2 = 0.5
two.divide(2); // 2 / 2 = 1
two.divide(3); // 3 / 2 = 1.5
two.divide(10, 5); // 10 / 5 = 2
Power(幂运算)
two.power(); // 2
two.power(3); // 8
two.power(10); // 1024
Square(开平方)
two.square(); // 1
two.square(4); // 2
two.square(1024); // 32
Different radices(基数)
two.base(2); // 10
two.base(8); // 2
two.base(10); // 2
two.base(16); // 2
# the base is 2(二进制)
two.baseOf(); // 01  # default 1
two.baseOf(10); // 1010
Different sorts of 2(花式输出2)
two.upHigh(); // ²
two.downLow(); // ₂
two.roman(); // Ⅱ
two.chinese(); // 二
two.chinese("pinyin"); // èr
two.chinese("financial"); // 贰
two.japanese(); // 二
two.english(); // two
two.upperCase(); // TWO
two.repeat();  // 2
two.repeat(5); // 22222
two.repeat(10); // 2222222222
Date(日期相关)
two.dayOfWeek(); // 周二
two.dayOfWeek("EN"); // Monday
two.monthOfYear(); // 二月
two.monthOfYear("EN"); // February
Unicode emoji(emoji表情)
two.peace(); // ✌️
two.victory(); // ✌️
two.eyes(); // 👀
two.oclock(); // 🕑
two.oclockStatus(); // 🛌 # default 2:00 am, you should be sleeping in the bed.(默认是凌晨两点,你应该在睡觉。)
two.oclockStatus("PM"); // 👨‍💻 # 2:00 pm, you shoulding be coding.(传参表示下午两点,你居然不在写代码?!)
two.oclockStatus("pm"); // 👨‍💻
Assert(判断)
two.isTwo();  // true
two.isTwo(2); // true
two.isTwo(3); // false
Compare(比较)
two.bigger(1, 2); // 2
two.smaller(1, 2); // 1

发布项目

完成项目的基本功能之后,就可以发布了,这时候执行:

npm publish

在这时,可能会报错,因为还没登录npm账户,那就执行 npm login 输入用户名密码即可:
image
最后,发布就行了,成功的截图如下:
_20190126211009

验证

验证的方法也很简单:

方法一:通过npm install 命令来检验是否可以安装
方法二:直接去npm网站进行搜索,记得要用全名搜索,并且要等一段时间再去搜,才能搜到。

接着,使用RunKit验证就行了:
_20190126211532

项目地址

https://github.com/monkey-play/two

2018/04/11--Java转义工具类StringEscapeUtils的介绍以及使用

在java.commons.lang3的包中有许多方便好用的工具类,类似于处理字符串的StringUtils,处理日期的DateUtils等等,StringEscapeUtils也是其中的一员。

StringEscapeUtils是在java.commons.lang3的2.0版本中加入的工具类,在3.6版本中被标注为@deprecated,表明在之后的版本中则为过时状态,之后StringEscapeUtils类被移到java.commons.text包下。

1. 功能用途

StringEscapeUtils的主要功能就是为Java,Java Script,Html,XML进行转义与反转义。

  • escapeJava(String input) / unescapeJava(String unionCodeString)
    将输入字符串转为unicode编码 / 将unicode字符串转为Utf-8格式的字符串
  • escapeHtml4(String input) / unescapeHtml4(String input)
    转义/反转义html脚本
  • escapeEcmaScript(String input) / unescapeEcmaScript(String input)
    转义/反转义js脚本
  • escapeXml(String input) / unescapeXml(String input)
    转义/反转义xml脚本

除了列出的几个较常用的方法,还有escapeJson(String input) / unescapeJson(String input)、escapeCsv(String input) / unescapeCsv(String input)等等,可以看一下下面的执行例子,有个直观的认识。

2. 执行例子

在项目中引入java.commons.text包(版本号可以访问官网选择):

gradle:

// https://mvnrepository.com/artifact/org.apache.commons/commons-text
compile group: 'org.apache.commons', name: 'commons-text', version: '1.3'

maven:

<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-text -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-text</artifactId>
    <version>1.3</version>
</dependency>
import org.apache.commons.text.StringEscapeUtils;
import org.junit.Test;

/**
 * @author liuqian
 * @date 2018/4/3 16:27
 */
public class EscapeTest {

  @Test
  public void escapeTest() {
    System.out.println("转义/反转义Java字符串");
    String javaString = "这是Java字符串";
    System.out.println(StringEscapeUtils.escapeJava(javaString));
    System.out.println(StringEscapeUtils.unescapeJava(StringEscapeUtils.escapeJava(javaString)));
    System.out.println("-------------------------------------------------------------");
    System.out.println("转义/反转义Json字符串");
    String jsonString = "{\"keyword\": \"这是Json字符串\"}";
    System.out.println(StringEscapeUtils.escapeJson(jsonString));
    System.out.println(StringEscapeUtils.unescapeJson(StringEscapeUtils.escapeJson(jsonString)));
    System.out.println("-------------------------------------------------------------");
    //除了html4还有html3等格式
    System.out.println("转义/反转义Html字符串");
    String htmlString = "<strong>加粗字符</strong>";
    System.out.println(StringEscapeUtils.escapeHtml4(htmlString));
    System.out.println(StringEscapeUtils.unescapeHtml4(StringEscapeUtils.escapeHtml4(htmlString)));
    System.out.println("-------------------------------------------------------------");
    //除了xml10还有xml11等格式
    System.out.println("转义/反转义xml字符串");
    String xmlString = "<xml>\"xml字符串\"</xml>";
    System.out.println(StringEscapeUtils.escapeXml10(xmlString));
    System.out.println(StringEscapeUtils.unescapeXml(StringEscapeUtils.escapeXml10(xmlString)));
    System.out.println("-------------------------------------------------------------");
    System.out.println("转义/反转义csv字符串");
    String csvString = "1997,Ford,E350,\"Super, luxurious truck\"";
    System.out.println(StringEscapeUtils.escapeCsv(csvString));
    System.out.println(StringEscapeUtils.unescapeCsv(StringEscapeUtils.escapeCsv(csvString)));
    System.out.println("-------------------------------------------------------------");
    System.out.println("转义/反转义Java Script字符串");
    String jsString = "<script>alert('1111')</script>";
    System.out.println(StringEscapeUtils.escapeEcmaScript(jsString));
   System.out.println(StringEscapeUtils.unescapeEcmaScript(StringEscapeUtils.escapeEcmaScript(jsString)));
  }
}

结果如下:

转义/反转义Java字符串
\u8FD9\u662FJava\u5B57\u7B26\u4E32
这是Java字符串
-------------------------------------------------------------
转义/反转义Json字符串
{\"keyword\": \"\u8FD9\u662FJson\u5B57\u7B26\u4E32\"}
{\"keyword\": \"这是Json字符串\"}
-------------------------------------------------------------
转义/反转义Html字符串
&lt;strong&gt;加粗字符&lt;/strong&gt;
<strong>加粗字符</strong>
-------------------------------------------------------------
转义/反转义xml字符串
&lt;xml&gt;&quot;xml字符串&quot;&lt;/xml&gt;
<xml>"xml字符串"</xml>
-------------------------------------------------------------
转义/反转义csv字符串
"1997,Ford,E350,""Super, luxurious truck"""
1997,Ford,E350,"Super, luxurious truck"
-------------------------------------------------------------
转义/反转义Java Script字符串
<script>alert(\'1111\')<\/script>
<script>alert('1111')</script>

2018/10/19--Javascript导出json数据为csv格式

在页面中如何将table中的json数据导出为csv文件呢?下面一个方法即可搞定:

csvExport: function(jsonData) {
      const replacer = (key, value) => (value === null ? "" : value);
      const header = Object.keys(jsonData[0]);
      var csv = jsonData.map(row =>
        header
          .map(fieldName => JSON.stringify(row[fieldName], replacer))
          .join(",")
      );
      csv.unshift(header.join(","));
      csv = csv.join("\r\n");
      csv = "data:text/csv;charset=utf-8,\uFEFF" + csv;
      console.log(csv);
      const link = document.createElement("a");
      link.href = encodeURI(csv);
      link.download = `filename.csv`;
      document.body.appendChild(link); // Required for FF
      link.click(); // This will download the data file named 'my_data.csv'.
      document.body.removeChild(link); // Required for FF
    }

2019/05/19--由 LeetCode 初见位运算

由三道 LeetCode 题目简单了解一下位运算

你可做过这几道题?

在面试的准备过程中,刷算法题算是必修课,当然我也不例外。某天,我刷到了一道神奇的题目:

# 136. 只出现一次的数字
给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。

说明:
你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?

示例 1:
输入: [2,2,1]
输出: 1

示例 2:
输入: [4,1,2,1,2]
输出: 4

我不禁眉头一皱,心说,这还不简单,三下五除二写下如下代码:

  /**
   * HashMap
   *
   * @param nums 数组
   * @return 结果
   */
  public int solution(int[] nums) {
    Map<Integer, Integer> map = new HashMap<>();
    for (int num : nums) {
      if (map.containsKey(num)) {
        map.remove(num);
      } else {
        map.put(num, 1);
      }
    }
    return map.entrySet().iterator().next().getKey();
  }

接着,我看到了另外一道题目:

# 137. 只出现一次的数字 II
给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现了三次。找出那个只出现了一次的元素。

说明:
你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?

示例 1:
输入: [2,2,3,2]
输出: 3

示例 2:
输入: [0,1,0,1,0,1,99]
输出: 99

我不禁眉头又一皱,心说,好像是同样的套路,便写下了如下代码:

  /**
   * 使用Map,存储key以及出现次数
   *
   * @param nums 数组
   * @return 出现一次的数字
   */
  public int singleNumber(int[] nums) {
    Map<Integer, Integer> map = new HashMap<>();
    for (int num : nums) {
      if (map.containsKey(num)) {
        map.put(num, map.get(num) + 1);
      } else {
        map.put(num, 1);
      }
    }
    for (Integer key : map.keySet()) {
      if (map.get(key) == 1) {
        return key;
      }
    }
    return 0;
  }

然后,就出现了终极题目:

# 260. 只出现一次的数字 III
给定一个整数数组 nums,其中恰好有两个元素只出现一次,其余所有元素均出现两次。 找出只出现一次的那两个元素。

示例 :
输入: [1,2,1,3,2,5]
输出: [3,5]

注意:
1. 结果输出的顺序并不重要,对于上面的例子, [5, 3] 也是正确答案。
2. 你的算法应该具有线性时间复杂度。你能否仅使用常数空间复杂度来实现?

我不禁又皱了一下眉头,心说,嗯……接着便写下如下代码:

  /**
   * 使用Map,存储key以及出现次数
   *
   * @param nums 数组
   * @return 出现一次的数字的数组
   */
  public int[] singleNumber(int[] nums) {
    int[] result = new int[2];
    Map<Integer, Integer> map = new HashMap<>();
    for (int num : nums) {
      if (map.containsKey(num)) {
        map.put(num, map.get(num) + 1);
      } else {
        map.put(num, 1);
      }
    }
    int i = 0;
    for (Integer key : map.keySet()) {
      if (map.get(key) == 1) {
        result[i] = key;
        i++;
      }
    }
    return result;
  }

用几乎同一种思路做了三道题,不得不夸一下自己:

11

做完这三道题目,提交了答案之后,执行用时和内存消耗都只超过了 10% 的解题者。不由得眉头紧锁(终于知道自己为啥抬头纹这么深了),发现事情并没有这么简单……

之后我又找了一下其他解法,如下:

  /**
   * #136 根据题目描述,由于加上了时间复杂度必须是 O(n) ,并且空间复杂度为 O(1) 的条件,因此不能用排序方法,也不能使用 map 数据结构。答案是使用 位操作Bit Operation 来解此题。
   * 将所有元素做异或运算,即a[1] ⊕ a[2] ⊕ a[3] ⊕ …⊕ a[n],所得的结果就是那个只出现一次的数字,时间复杂度为O(n)。
   * 根据异或的性质 任何一个数字异或它自己都等于 0
   *
   * @param nums 数组
   * @return 结果
   */
  private int solution(int[] nums) {
    int res = 0;
    for (int num : nums) {
      res ^= num;
    }
    return res;
  }
  /**
   * #137 嗯……这个我们下面再做详解
   * 这里使用了异或、与、取反这些运算
   *
   * @param nums 数组
   * @return 出现一次的数字
   */
  public int singleNumber2(int[] nums) {
    int a = 0, b = 0;
    int mask;
    for (int num : nums) {
      b ^= a & num;
      a ^= num;
      mask = ~(a & b);
      a &= mask;
      b &= mask;
    }
    return a;
  }
  /**
   * #260 在这里把所有元素都异或,那么得到的结果就是那两个只出现一次的元素异或的结果。
   * 然后,因为这两个只出现一次的元素一定是不相同的,所以这两个元素的二进制形式肯定至少有某一位是不同的,即一个为 0 ,另一个为 1 ,现在需要找到这一位。
   * 根据异或的性质 任何一个数字异或它自己都等于 0 ,得到这个数字二进制形式中任意一个为 1 的位都是我们要找的那一位。
   * 再然后,以这一位是 1 还是 0 为标准,将数组的 n 个元素分成两部分。
   * 1. 将这一位为 0 的所有元素做异或,得出的数就是只出现一次的数中的一个
   * 2. 将这一位为 1 的所有元素做异或,得出的数就是只出现一次的数中的另一个。
   * 这样就解出题目。忽略寻找不同位的过程,总共遍历数组两次,时间复杂度为O(n)。
   * 
   * 使用位运算
   *
   * @param nums 数组
   * @return 只出现一次数字的数组
   */
  public int[] singleNumber2(int[] nums) {
    int diff = 0;
    for (int num : nums) {
      diff ^= num;
    }
    // 得到最低的有效位,即两个数不同的那一位
    diff &= -diff;
    int[] result = new int[2];
    for (int num : nums) {
      if ((num & diff) == 0) {
        result[0] ^= num;
      } else {
        result[1] ^= num;
      }
    }
    return result;
  }

看完上面的解法,我脑海中只有问号的存在,啥意思啊?!

下面就让我们简单了解一下位运算并解析一下这三道题目。

简单介绍一下位运算

1. 异或运算(^)

异或逻辑的关系是:当AB不同时,输出P=1;当AB相同时,输出P=0。“⊕”是异或数学运算符号,异或逻辑也是与或非逻辑的组合,其逻辑表达式为:P=A⊕B。在计算机语言中,异或的符号为“ ^ ”。

异或运算 A ⊕ B 的真值表如下:

A B
F F F
F T T
T F T
T T F

所以我们从 #136 题解中了解,通过异或运算,两个相同的元素结果为 0,而 任何数0 进行异或操作,结果都为其本身。

2. 与操作(&)

“与”运算是计算机中一种基本的逻辑运算方式,符号表示为 “&”,参加运算的两个数据,按二进制位进行“与”运算。运算规则:0&0=0;0&1=0;1&0=0;1&1=1;即:两位同时为“1”,结果才为“1”,否则为0。另,负数按补码形式参加按位与运算。

与运算 A & B 的真值表如下:

A B &
F F F
F T F
T F F
T T T

“与运算”的特殊用途:

  1. 清零。如果想将一个单元清零,即使其全部二进制位为0,只要与一个各位都为零的数值相与,结果为零。

  2. 取一个数的指定位

    方法:找一个数,对应X要取的位,该数的对应位为1,其余位为零,此数与X进行“与运算”可以得到X中的指定位。例:设 X=10101110,取X的低4位,用 X & 0000 1111 = 0000 1110 即可得到;还可用来取 X 的2、4、6位。

3. 或操作(|)

参加运算的两个对象,按二进制位进行“或”运算。运算规则:0|0=0; 0|1=1; 1|0=1; 1|1=1;即 :参加运算的两个对象只要有一个为1,其值为1。另,负数按补码形式参加按位或运算。

或运算 A | B 的真值表如下:

A B |
F F F
F T T
T F T
T T T

或运算特殊作用:

  1. 常用来对一个数据的某些位置1。

    方法:找到一个数,对应X要置1的位,该数的对应位为1,其余位为零。此数与X相或可使X中的某些位置1。

    例:将 X=10100000 的低4位 置为1 ,用 X | 0000 1111 = 1010 1111 即可得到。

4. 取反操作(~

参加运算的一个数据,按二进制位进行“取反”运算。运算规则:~1=0; ~0=1;即:对一个二进制数按位取反,即将0变1,1变0。

使一个数的最低位为零,可以表示为:a&~11 的值为 1111111111111110,再按“与”运算,最低位一定为0。因为“”运算符的优先级比算术运算符、关系运算符、逻辑运算符和其他运算符都高。


OK,截止到这儿,三道题目中使用的位运算介绍完毕,那么这里我们插入一下 #137 的详细题解。

public int singleNumber2(int[] nums) {
    // 这里我们改一下变量名
    // 用 one 记录到当前处理的元素为止,二进制1出现“1次”(mod 3 之后的 1)的有哪些二进制位;
    // 用 two 记录到当前计算的变量为止,二进制1出现“2次”(mod 3 之后的 2)的有哪些二进制位。
    int one = 0, two = 0;
    int mask;
    for (int num : nums) {
      // 由于 two 要考虑,one 的已有状态,和当前是否继续出现。所以要先算
      two ^= one & num;
      // one 就是一个0,1的二值位,在两个状态间转换
      one ^= num;
      // 当 one 和 two 中的某一位同时为1时表示该二进制位上1出现了3次,此时需要清零。
      mask = ~(one & two);
      // 清零操作
      one &= mask;
      two &= mask;
    }
    // 即用 二进制 模拟 三进制 运算。最终 one 记录的是最终结果。
    return one;
  }

首先考虑一个相对简单的问题,加入输入数组里面只有 0 和 1,我们要统计 1 出现的次数,当遇到 1 就次数加 1,遇到 0 就不变,当次数达到 k 时,统计次数又回归到 0。我们可以用 m 位来做这个计数工作,即 xm, xm−1, …, x1,只需要确保 2 > k 即可,接下来我们要考虑的问题就是,在每一次check元素的时候,做什么操作可以满足上述的条件。在开始计数之前,每一个计数位都初始化位0,然后遍历nums,直到遇到第一个1,此时 x1 会变成1,继续遍历,直到遇到第二个1,此时 x1=0, x2=1,直到这里应该可以看出规律了。每遇到一个1,对于 xm, xm−1, …, x1,只有之前的所有位都为1的时候才需要改变自己的值,如果本来是1,就变成0,本来是0,就变成1 ,如果遇到的是0,就保持不变。搞清楚了这个逻辑,写出表达式就不难了。这里以 m = 3 为例给出 java 代码:

for(int num: nums) {
    x3 ^= x2 & x1 & i;
    x2 ^= x1 & i;
    x1 ^= i;
    // other operations
}

但是到这里还没有解决当 1 的次数到 k 时,计数值要重新返回到 0,也就是所有计数位都变成 0 这个问题。解决办法也是比较巧妙。

假设我们有一个标志变量,只有当计数值到 k 的时候这个标志变量才为 0,其余情况下都是 1,然后每一次check元素的时候都对每个计数位和标志变量做与操作,那么如果标志变量为 0,也就是计数值为 k 的时候,所有位都会变成 0, 反之,所有位都会保持不变,那么我们的目的也就达到了。

好,最后一个问题是怎么计算标志变量的值。将 k 转变为二进制,只有计数值达到 k,所有计数位才会和 k 的二进制一样,所以只需要将 k 的二进制位做 与操作 ,如果某个位为 0,就与该位 取反 之后的值做与操作。

以 k=3, m=2 为例,简要的 java 代码如下:

// where yj = xj if kj = 1, 
// and yj = ~xj if kj = 0, 
// k1, k2是 k 的二进制表示(j = 1 to 2). 
mask = ~(y1 & y2); 
x2 &= mask;
x1 &= mask;

将这两部分合起来就是解决这个问题的完整算法了。


5. 左移运算符(<<)

将一个运算对象的各二进制位全部左移若干位(左边的二进制位丢弃,右边补0)。

例:a = a<< 2将a的二进制位左移2位,右补0,左移1位后a = a * 2;

若左移时舍弃的高位不包含1,则每左移一位,相当于该数乘以2。

6. 右移运算符(>>)

将一个数的各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃。操作数每右移一位,相当于该数除以2。

例如:a = a>> 2 将a的二进制位右移2位,左补0 or 补1得看被移数是正还是负。

总结

以上就是我们常见的几种位运算了,其中左移、右移等操作,在 HashMap的源码中也会经常看到,理解了这些位操作,对于理解源码也是有一定帮助的,当然也会帮助我们写出执行效率更高的代码。

从上面的部分示例中可以看出,位运算通常用来降低包含排列,计数等复杂度比较高的操作,当然也可以用来代替乘 2 除 2,判断素数,偶数,倍数等基本操作,但是我认为其意义在于前者,即用计数器来降低设计到排列或者计数的问题的复杂度。

最后一点,三道算法题中,#136#260 理解起来倒还好,#137 Single Number II 的题解可能需要费一点功夫,至少我还没有完全理解,但不能轻易放弃对不对,继续啃啊!

以上便是我个人的简单总结,如果有纰漏或者错误,欢迎进行指出及纠正。

参考:

2017/11/26--机器学习初体验

翻译:The Missing Roadmap to Self-Study Machine Learning

原文地址: https://machinelearningmastery.com/machine-learning-roadmap-your-self-study-guide-to-machine-learning/

给自学机器学习者补上的一张路线图

在这篇它帖子里我将列出具体的用于自学机器学习的路线图,你可以用来找到自己的定位并且决定下一步怎么做。

我想了很多关于框架和系统的(学习)方法(正如我在博客上表明的一样)。之前我的一篇帖子,“机器学习的自学导引(Self-Study Guide to Machine Learning)”,在社区中引起了不小的轰动,而这篇贴子我会当成是对之前自学编程**的一次扩充说明。

来让我们跳进来吧……

机器学习路线图

机器学习是研究的一个巨大领域。有如此繁多的算法、理论、技术以及经典问题需要去学习了解,以致于让人喘不过气。

机器学习同样是需要深度跨学科的。你会从一个倾向开发人员的人,转变成为倾向为统计学家,而且当有如此多先前的知识(需要学习)时确实令人泄气。

你需要的是一个结构化的方法,不仅能给出机器学习中细节的学习主题和等级的一张路线图,也能整合类似书籍和开源的一些受欢迎的资源。

结构化的方法把重点放在了你要学什么以及你何时需要学的问题上,并由此标明了那些压倒性的问题。另外通过给资料介绍按照实际用途排序,由此标明哪些地方容易受挫,简直为工程师和编程人员量身定制。

一份路线图可以让你给自己指引你在哪儿和你想去哪儿。

自学是条道

自学意味着按照你自己的步幅,你自己的目标,以及你自己的计划学习。

自学是机器学习最好的途径。但那并不是指你事事都要亲力亲为,恰恰相反,它是指(自学)对你而言是最有效率的一种方式,并且可以借力于网络上最好的课程、书籍以及有用的指导去学习。

自学同样兼容比如大学本科和研究生学习的较为正式的课程。这表明着自学正积极整合这些材料入你的知识库,并拥有着这一过程。在这个过程中,你可以更加深入地探索那些最打动你的区域(领域)。

机器学习是一门应用学科,就像编程。学习理论知识固然重要,但你也必须花费时间去实践理论。你必须练习,这很紧急。你需要建立起对进程、算法以及问题的直觉。

能力等级

学习机器学习的结构化方法被分为如下四个能力的等级:

  1. 入门者
  2. 初学者
  3. 中级
  4. 高级
    这四个等级根据他们面对的问题和他们学习的目标来划定的。相对的,在他们追求目标的道路上,每个等级都有一系列不同的活动。

每一等级的难题

每一个能力等级都会面临一系列不同的问题,如下:

  • 入门者:疑惑何为真正的机器学习。被大量的可用信息所淹没。在大部分可用信息中,对未知的假设的先验知识感到沮丧。
  • 初学者:被算法的数学描述吓倒。致力于解决机器学习应用的难题。迷失于通过寻找问题来探究机器学习。
  • 中级:对介绍性的资料感到无聊。而对更多细节和更深入的观点的需求若饥若渴。渴望展现并向前推动他们的知识和技巧。
  • 高级:痴迷于从系统以及解决方案中获取最大收益。寻求更大贡献的机会。对推动(机器学习的)界限振奋不已。

学习目标

在能力等级中的每个级别都有一个单一的目标,并且有许多小任务促使他们完成这一目标。目标如下:

  • 入门者:打好基础,然后开始旅程。
  • 初学者:开发并实践应用机器学习的过程。
  • 中级:变得对算法、问题以及工具有更深入的理解。
  • 高级:达到能够对该领域进行扩展,比如在算法、问题及工具方面。

自学的做法

每个等级的目标决定了各种各样的计划安排来实现这些目标。你可以安排你自己的活动(这是极力推荐的),尽管下面是针对每个等级建议的计划。
入门者

  • 发现机器学习中的“为什么们”(比如为什么机器学习重要及为什么它对你来说重要)。
  • 找出可能阻碍你的自我限制的念头(比如,没有学位,数学不好)。
  • 探究本领域的基础定义和概念(比如机器学习问题和算法)。
    初学者
  • 研究并学习应用机器学习过程中的步骤。
  • 通过应用机器学习的步骤,充分理解一个工具或者类库工作的细节(基本熟悉工具和类库)。
  • 实践应用机器学习中端到端问题的过程。

中级

  • 对算法、问题和工具进行小范围的调查。
  • 通过从竞争机器学习和不断练习提高应用机器学习的技术。

高级

  • 用结构化的方法拓展算法、问题和工具。
  • 为社区做出贡献。

使用方法

这份路线图是一个非常有用的工具,在你征服机器学习的路上,你会在各个方面都用得上的:

  • 学习指南:把它当做目标和计划的线性指标来完成。耐心和努力会让你在短期内就达到高级水平。
  • 精简指南:像上面一样当做线性指标完成,但需要把目标缩小到你正在掌握的机器学习范围内,而不是更加广泛的应用机器学习领域。可以是特定的问题或者一类算法。
  • 信息过滤器:这路线图可以用来过滤你遇到的信息和资源。这是一个非常有用的用途,因为在学习过程中你能快速地评判哪篇博客帖子,哪篇文章或者哪本书与你的能力等级相对应的。

这条路是适合你的

我也为其他的工程师和程序员设计了这份指南。

  • 你可能知道怎么编程。
  • 你可能是(或曾是)个专业的工程师或者程序员。
  • 你可能是本科生或者研究生。
  • 你对机器学习或数据学很感兴趣。
  • 你可能正做着与机器学习和数据相关的工作。
    这个方法为那些已经熟知开发和构建一个系统的过程的程序员和工程师。他们能用一种计算或逻辑性的方式去思考,并且能系统地思考。程序员尤其熟悉自动化的力量和算法的复杂度及表征。

这个方法不仅对于专业的程序员而且学习工程学,计算机科学或者类似学科的学生都非常有用。

  • 你没必要成为程序员或者优秀程序员。你可以用现成的工具,比如有图形化接口的Weka,进而解决机器学习问题并使用机器学习算法。
  • 你没必要成为数学家或者统计学家。当到了需要研究给定的算法时,你只要学习统计学,概率学和线性代数就好。
  • 你可以阅读指南,读书,以及看公开课。它们很像是对四种能力水平的细分内容。一本给定的书或许对初学者或中级水平,甚至是各个等级,是一份完美的参考书。相似的,一门课程或许契合给定的一个等级或者可能跨两个或更多等级。这些都给出了各种机器学习活动的样本。

范围

我建议你把范围集中在分类和回归类型的问题以及相应的算法和工具上。这是两个最常见的基础机器学习问题,而且其他问题都可以缩小到这个范围内。
这有一些机器学习的子领域,比如计算机视觉,自然语言处理,导购系统或者强化学习。这些领域都可以被缩小至分类和回归问题,并且它们的学习(过程)特别适合当前的路线图结构。我会建议你除非你到了中级水准,否则不要进入这些领域。

原则

~机器学习是一次旅行。你学知道你当前的位置以及你正试图去哪里。这是需要时间和努力的,但是这对你也有很多帮助。

  • 创造一个半正式的工作产品。以博客帖子,技术报告,以及代码仓库等形式写下你一路上学到的、发现的,你会很快积攒一些论证过的技能和知识不仅仅是为你自己,也可以提供给其他人参考。
  • 即时学习。除非到了你需要的时候,否则不要学习那些复杂的主题。例如,学习足够的概率论与线性代数来理解你当前正研究的算法,而不要在开始机器学习之前花3年的时间去学习统计和数学的课程。
  • 利用现有的技术。如果你会写代码,去实现要好于通过研究数学来理解算法。使用你知道的语言。集中注意力你在学习的一件事上,不要与之同时再去学习一门新的语言、工具或者资料复杂化学习过程。
  • 完全掌握只是理想状态。机器学习的掌握是需要持续学习。你永远不能达到,你只能持续研究,学习并且提高。

技巧要点

下面是可以更加有效地让你在这篇指导以及学习机器学习的过程之外得到更多的3个技巧:

  • 以一个你可以一小时完成的小项目开始。
  • 力争每周完成一个项目用以建立并维持势头,并且完成一个你可以构建的项目工作区。
  • 在你的博客、Facebook、Google+、GitHub或者其他任何可以展示你兴趣的地方分享你学习的成果,进而提升技术、知识并得到反馈。
    行动步骤

花一点时间写一下:

  • 你认为你目前是什么水平以及你正解决着什么问题?
  • 你想达到什么等级以及能够做成什么事?

2017/12/12--Superset使用手册

前言: 本文主要介绍Superset的安装以及使用,更主要的是如何使用SQL Lab进行数据查询和数据可视化的任务。此外,文末也会介绍一些权限分配的一些应用场景。

2017/12/07--翻译:人工智能(AI)与机器学习的不同

原文地址:https://www.forbes.com/sites/bernardmarr/2016/12/06/what-is-the-difference-between-artificial-intelligence-and-machine-learning/#1d71a8c02742

人工智能(AI)和机器学习是当下最热门的两个词,而且含义上经常被互换使用。

虽然它们并不等同,但是有时候在感觉上也是会造成一定混乱的。所以我认为还是值得写一篇文章来解释它们的不同之处。

两者频繁出现的时刻都是在谈论的话题如大数据,分析学以及正席卷全球的技术变革的广泛冲击。

简言之,最好的答案是:
机器可以用一种我们认为“聪明”的方式去完成任务,人工智能则是其更宽泛的概念。
以及,
AI是基于我们应该仅让机器访问数据,然后它们便能自己学习这一创意,机器学习只是当前的一种应用。

早些年

人工智能已经存在很长时间了——希腊神话中也有机械人模仿人类行为的故事。早期的欧洲的计算机是被设想为“逻辑机器”的,由于类似基础算术和记忆的再现能力,工程师从根本上把自己的工作视为试图创造机械的大脑。

由于当代技术,这是很重要的,我们对于人类思维运作的理解也在发展,从而关于AI构成的概念亦发生了变化。AI领域的工作,集中在模仿人类的决策产生过程以及更多地以人类的方式去完成任务,而不是持续增长的复杂计算。

人工智能——旨在表现得智能的设备——经常被归为应用的或通用这两个基本组别的之一。应用AI更为常见——系统设计得更为智能地交易股票和股份,或者自动驾驶车辆的演示也属于这一类。

Artificial Intelligence

一般的人工智能——系统也好设备也好,理论上可以处理任何任务,然而并不常见。但这也是当今正在发生的最令人激动的可进步之处,同时也是带动机器学习发展的领域。经常作为AI的子领域被提及,把(机器学习)当做是目前最新的技术的确更为准确。

机器学习的起势

两项重要突破导致了机器学习的出现,而这也正驱动着AI以目前的(飞快)速度向前发展。

二者之一是——曾在1959年由Arthur Samuel提出这一概念,(机器学习)不仅仅是传授给计算机它们需要了解的一切以及怎样完成任务,更是产生了它们自学的可能性。

第二项突破则距今不远,那就是互联网的出现,以及数字信息在数量上的剧增,存储,并且可用于分析。

曾经这些创新提出之后,工程师们意识到不单单是教给计算机和机器怎么做事,甚至可以通过编程让它们像人类一样思考,然后将它们加入互联网以获得世界上所有的信息。

神经网络

神经网络的发展已成为教授计算机用人类的方式思考和理解世界的关键,与此同时它们还有着优于我们的天赋,比如(计算)速度,准确性,以及毫无偏见。

一个神经网络就是一套计算机系统,旨在以和人类大脑分类信息的方式运行。依靠含有的“神经元”,它们可以学会辨认类似图片并且把图片进行分类。

实质上它运行在一套概率系统上,这套系统由数据支撑,并且可以发表声明,做出决定,或者给出有一定把握的预测。一个反馈回路可以通过传感或者被告知进行“学习”,这一附加属性,无论它的决定是否正确,都会改变未来所使用的方法。

机器学习应用可以阅读文章并判别作者的意图是给出投诉还是表达祝贺。它们还可以听一段音乐,然后判定这段音乐是否可能让人开心或者难过,之后找出其他的音乐来匹配情感。在一些例子中,它们甚至可以谱出自己的音乐来表达同样的主题,或者用它们熟知的一些会被那些发烧友喜爱的音乐原件。

这些所有的可能性都由基于机器学习和神经网络的系统提供。非常感谢科幻小说,我们或许可以与电子设备和数字信息相互交流、影响,这一设想也被照进了现实,正如我们和一个人类所做的那样自然。最后,AI的另一个领域——自然语言处理(Natural Language Processing (NLP))——近年来已经成为那些让人非常激动革新的原动力,并且是非常依赖ML的领域之一。

NLP应用试图去理解正常的人类交流,不论是书面还是口头的,并且用和人类相似的自然语言来回应。ML在这里用来帮助机器理解人类语言的中大量的细微差别之处,以及学习用一种能让某位特定听众理解的方式去回应对方。

人工智能——尤其是当今的机器学习(ML)自然能提供很多东西。由于其达成重复劳动自动化以及提供创造性眼光的承诺,从银行业到医疗与制造业等各个行业都在从中受益。因此,重要的是要记住,AI和ML是别的东西……而它们是正被出售的产品,一直都是,且有利可图。

机器学习当然已被营销人员抓住了机遇。AI已经出现了如此之久,而在其潜力真正被发掘之前,它甚至可能开始被视作“老一套”的东西。在“AI革命”开始出现一些错误的开始,然后“机器学习”这一术语给了营销人员一些新鲜、闪亮的东西,并且重要的是,牢牢扎根于此时此地。

事实就是我们最终会开发出与人类相似的AI,而这也经常被技术人员视为不可避免的东西。当然,今天我们以与日俱增的速度,未曾如此的靠近这一目标。近些年来我们所看到的许多令人激动的进展,正是由于我们如何设想AI运行的根本改变,同时也是ML所带来的改变。我希望这篇文章能够帮助一些人理解AI与ML之间的区别。

2018/05/12--【Mysql(SQL语句)】当两个表的字符集不同时的关联方法

某天,在我执行某句关联查询的SQL的时候,突然报了一个如下错误,然后找到了解决的方案,在这里记录一下。

【执行的SQL】SELECT a.id, a.user_num, b.name FROM tablaA a LEFT JOIN tableB b on a.user_num = b.user_id;

【错误信息】Illegal mix of collations (utf8_unicode_ci,IMPLICIT) and (utf8_general_ci,IMPLICIT) for operation '='

【错误分析】错误信息显示由于非法的字符排序规则(collations )混用而不能进行'='操作,那么就应该是一张表的collations是‘utf8_unicode_ci’,而另一张表的collations是‘utf8_general_ci’,那么要做的就是统一两张表的collations。

【更改后的SQL】SELECT a.id, a.user_num, b.name FROM tablaA a LEFT JOIN tableB b on a.user_num COLLATE utf8_general_ci = b.user_id COLLATE utf8_general_ci;,在SQL关联的字段上加上统一的collations即可,比如COLLATE utf8_general_ci

【注】utf8_general_ci和utf8_unicode_ci的区别,前者校对速度快,但准确度稍差;后者准确度高,但校对速度稍慢。

参考:
https://stackoverflow.com/questions/45621178/illegal-mix-of-collations-utf8-unicode-ci-implicit-and-utf8-general-ci-implic?rq=1

https://stackoverflow.com/questions/11770074/illegal-mix-of-collations-utf8-unicode-ci-implicit-and-utf8-general-ci-implic

https://dba.stackexchange.com/questions/24587/mysql-illegal-mix-of-collations

https://blog.csdn.net/abandonship/article/details/46859043

2019/05/11--分享Java中通过 Stream 对列表去重的几个方法

几种列表去重的方法

在这里我来分享几种列表去重的方法,如有纰漏,请不吝赐教。

1. Stream 的distinct()方法

distinct()是Java 8 中 Stream 提供的方法,返回的是由该流中不同元素组成的流。distinct()使用 hashCode()eqauls() 方法来获取不同的元素。因此,需要去重的类必须实现 hashCode()equals() 方法。换句话讲,我们可以通过重写定制的 hashCode()equals() 方法来达到某些特殊需求的去重。

distinct() 方法声明如下:

Stream<T> distinct();

1.1 对于 String 列表的去重

因为 String 类已经覆写了 equals()hashCode() 方法,所以可以去重成功。

@Test
public void listDistinctByStreamDistinct() {
  // 1. 对于 String 列表去重
  List<String> stringList = new ArrayList<String>() {{
    add("A");
    add("A");
    add("B");
    add("B");
    add("C");
  }};
  out.print("去重前:");
  for (String s : stringList) {
    out.print(s);
  }
  out.println();
  stringList = stringList.stream().distinct().collect(Collectors.toList());
  out.print("去重后:");
  for (String s : stringList) {
    out.print(s);
  }
  out.println();
}

结果如下:

去重前:AABBC
去重后:ABC

1.2 对于实体类列表的去重

注:代码中我们使用了 Lombok 插件的 @Data注解,可自动覆写 equals() 以及 hashCode() 方法。

/**
* 定义一个实体类
*/ 
@Data
public class Student {
  private String stuNo;
  private String name;
}
@Test
public void listDistinctByStreamDistinct() throws JsonProcessingException {
    ObjectMapper objectMapper = new ObjectMapper();
    // 1. 对于 Student 列表去重
    List<Student> studentList = getStudentList();
    out.print("去重前:");
    out.println(objectMapper.writeValueAsString(studentList));
    studentList = studentList.stream().distinct().collect(Collectors.toList());
    out.print("去重后:");
    out.println(objectMapper.writeValueAsString(studentList));
  }

结果如下:

去重前:[{"stuNo":"001","name":"Tom"},{"stuNo":"002","name":"Mike"},{"stuNo":"001","name":"Tom"}]
去重后:[{"stuNo":"001","name":"Tom"},{"stuNo":"002","name":"Mike"}]

2. 根据 List<Object>Object 某个属性去重

2.1 新建一个列表出来

  @Test
  public void distinctByProperty1() throws JsonProcessingException {
    // 这里第一种方法我们通过新创建一个只有不同元素列表来实现根据对象某个属性去重
    ObjectMapper objectMapper = new ObjectMapper();
    List<Student> studentList = getStudentList();

    out.print("去重前        :");
    out.println(objectMapper.writeValueAsString(studentList));
    studentList = studentList.stream().distinct().collect(Collectors.toList());
    out.print("distinct去重后:");
    out.println(objectMapper.writeValueAsString(studentList));
    // 这里我们引入了两个静态方法,以及通过 TreeSet<> 来达到获取不同元素的效果
    // 1. import static java.util.stream.Collectors.collectingAndThen;
    // 2. import static java.util.stream.Collectors.toCollection;
    studentList = studentList.stream().collect(
      collectingAndThen(
        toCollection(() -> new TreeSet<>(Comparator.comparing(Student::getName))), ArrayList::new)
    );
    out.print("根据名字去重后 :");
    out.println(objectMapper.writeValueAsString(studentList));
  }

结果如下:

去重前        :[{"stuNo":"001","name":"Tom"},{"stuNo":"001","name":"Tom"},{"stuNo":"003","name":"Tom"}]
distinct去重后:[{"stuNo":"001","name":"Tom"},{"stuNo":"003","name":"Tom"}]
根据名字去重后 :[{"stuNo":"001","name":"Tom"}]

2.2 通过 filter() 方法

我们首先创建一个方法作为 Stream.filter() 的参数,其返回类型为 Predicate,原理就是判断一个元素能否加入到 Set 中去,代码如下:

private static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
    Set<Object> seen = ConcurrentHashMap.newKeySet();
    return t -> seen.add(keyExtractor.apply(t));
}

使用如下:

  @Test
  public void distinctByProperty2() throws JsonProcessingException {
    // 这里第二种方法我们通过过滤来实现根据对象某个属性去重
    ObjectMapper objectMapper = new ObjectMapper();
    List<Student> studentList = getStudentList();

    out.print("去重前        :");
    out.println(objectMapper.writeValueAsString(studentList));
    studentList = studentList.stream().distinct().collect(Collectors.toList());
    out.print("distinct去重后:");
    out.println(objectMapper.writeValueAsString(studentList));
    // 这里我们将 distinctByKey() 方法作为 filter() 的参数,过滤掉那些不能加入到 set 的元素
    studentList = studentList.stream().filter(distinctByKey(Student::getName)).collect(Collectors.toList());
    out.print("根据名字去重后 :");
    out.println(objectMapper.writeValueAsString(studentList));
  }

结果如下:

去重前        :[{"stuNo":"001","name":"Tom"},{"stuNo":"001","name":"Tom"},{"stuNo":"003","name":"Tom"}]
distinct去重后:[{"stuNo":"001","name":"Tom"},{"stuNo":"003","name":"Tom"}]
根据名字去重后 :[{"stuNo":"001","name":"Tom"}]

3. 总结

以上便是我要分享的几种关于列表去重的方法,当然这里没有进行更为详尽的性能分析,希望以后会深入底层再重新分析一下。如有纰漏,还望不吝赐教。

代码地址:github

2019/04/09--@Autowire和@Resource的区别

对比 @Autowire@Resource

  • @Autowire@Resource都可以用来装配bean,都可以用于字段或setter方法。
  • @Autowire 默认按类型装配,默认情况下必须要求依赖对象必须存在,如果要允许 null 值,可以设置它的 required 属性为 false。
  • @Resource 默认按名称装配,当找不到与名称匹配的 bean 时才按照类型进行装配。名称可以通过 name 属性指定,如果没有指定 name 属性,当注解写在字段上时,默认取字段名,当注解写在 setter 方法上时,默认取属性名进行装配。

注意:如果 name 属性一旦指定,就只会按照名称进行装配。

  • @Autowire@Qualifier配合使用效果和@Resource一样:
@Autowired(required = false) @Qualifier("example")
private Example example;

@Resource(name = "example")
private Example example;
  • @Resouce 装配顺序
  1. 如果同时指定 name 和 type,则从容器中查找唯一匹配的 bean 装配,找不到则抛出异常;
  2. 如果指定 name 属性,则从容器中查找名称匹配的 bean 装配,找不到则抛出异常;
  3. 如果指定 type 属性,则从容器中查找类型唯一匹配的 bean 装配,找不到或者找到多个抛出异常;
  4. 如果不指定,则自动按照 byName 方式装配,如果没有匹配,则回退一个原始类型进行匹配,如果匹配则自动装配。

简要对比表格

注解对比 @Resource @Autowire
注解来源 JDK Spring
装配方式 优先按名称 优先按类型
属性 name、type required

链接:https://blog.csdn.net/u012102104/article/details/79481007

2019/03/31--MySQL索引知多少?

MySQL索引知多少

索引是一个排序的列表,在这个列表中存储着索引的值和包含这个值的数据所在行的物理地址,在数据十分庞大的时候,索引可以大大加快查询的速度,这是因为使用索引后可以不用扫描全表来定位某行的数据,而是先通过索引表找到该行数据对应的物理地址然后访问相应的数据。

MySQL索引的建立对于MySQL的高效运行是很重要的,索引可以大大提高MySQL的检索速度。

但实际上,索引也是一张表,保存了主键与索引字段,并指向实体表的记录。因此索引也会有它的缺点:虽然索引大大提高了查询速度,同时却也会降低更新表的速度,如对表进行INSERT、UPDATE和DELETE。因为更新表时,MySQL不仅要保存数据,还要保存一下索引文件。建立索引也会占用磁盘空间的索引文件。

接下来此文便从MySQL中索引的语法索引的分类索引的实现原理索引的使用策略索引的优化索引的优缺点几部分详细介绍一下索引。

一、 MySQL中索引的语法

创建索引

  1. 直接创建普通索引
CREATE INDEX indexName ON mytable(username(length)); 

如果是CHAR,VARCHAR类型,length可以小于字段实际长度;如果是BLOB和TEXT类型,必须指定 length。

  1. 修改表结构(添加索引)
ALTER TABLE tableName ADD INDEX indexName(columnName);
  1. 创建表的时候直接指定
CREATE TABLE mytable(
  ID INT NOT NULL,
  username VARCHAR(16) NOT NULL,
  (UNIQUE) INDEX indexName(username(length))
);

删除索引

DROP (UNIQUE) INDEX indexName ON mytable;

显示索引信息

SHOW INDEX FROM tableName;

注:这里在语句里标明了UNIQUE的命令,即为唯一索引相关的命令。下文会有详细介绍。

二、 MySQL索引的分类

常见的索引类型有:主键索引、唯一索引、普通索引、全文索引、组合索引

  1. 主键索引:即主索引,根据主键pk_column(length)建立索引,不允许重复,不允许空值。
ALTER TABLE tableName ADD PRIMARY KEY(column);
  1. 唯一索引:索引列的值必须唯一,但允许有空值。如果是组合索引(即多列索引),则列值的组合必须唯一。
ALTER TABLE tableName ADD UNIQUE indexName (column_list);
  1. 普通索引:表中的普通列构建的索引,没有任何限制,索引值可出现多次。
ALTER TABLE tableName ADD INDEX indexName(column_list);
  1. 全文索引:用大文本对象的列构建的索引。
ALTER TABLE tableName ADD FULLTEXT indexName(column_list);
  1. 组合索引:用多个列组合构建的索引,这多个列中的值不允许有空值。
ALTER TABLE tableName ADD INDEX indexName(col1, col2, col3);

组合索引遵循“最左前缀”原则,把最常用作为检索或排序的列放在最左,依次递减,组合索引相当于建立了col1,col1col2,col1col2col3三个索引,而col2或者col3是不能使用索引的。

在使用组合索引的时候可能因为列名长度过长而导致索引的key太大,导致效率降低,在允许的情况下,可以只取col1和col2的前几个字符作为索引,如下:

ALTER TABLE 'table_name' ADD INDEX index_name(col1(4),col2(3));

表示使用col1的前4个字符和col2的前3个字符作为索引。

三、 索引的实现原理

MySQL支持诸多存储引擎,而各种存储引擎对索引的支持也各不相同,因此MySQL数据库支持多种索引类型,如BTree索引,B+Tree索引,哈希索引,全文索引等等。

哈希索引

只有memory(内存)存储引擎支持哈希索引,哈希索引用索引列的值计算该值的hashCode,然后在hashCode相应的位置存执该值所在行数据的物理位置,因为使用散列算法,因此访问速度非常快,但是一个值只能对应一个hashCode,而且是散列的分布方式,因此哈希索引不支持范围查找和排序的功能。

全文索引

FULLTEXT(全文)索引,仅可用于MyISAM和InnoDB,针对较大的数据,生成全文索引非常的消耗时间和空间。对于文本的大对象,或者较大的CHAR类型的数据,如果使用普通索引,那么匹配文本前几个字符还是可行的,但是想要匹配文本中间的几个单词,那么就要使用LIKE %word%来匹配,这样需要很长的时间来处理,响应时间会大大增加,这种情况,就可使用时FULLTEXT索引了,在生成FULLTEXT索引时,会为文本生成一份单词的清单,在索引时及根据这个单词的清单来索引。

  • 对于较大的数据集,把数据添加到一个没有FULLTEXT索引的表,然后添加FULLTEXT索引的速度比把数据添加到一个已经有FULLTEXT索引的表快。
  • MySQL自带的全文索引只能用于MyISAM存储引擎,如果是其它数据引擎,那么全文索引不会生效。
  • 在MySQL中,全文索引支队英文有用,目前对中文还不支持。
  • 在MySQL中,如果检索的字符串太短则无法检索得到预期的结果,检索的字符串长度至少为4字节,此外,如果检索的字符包括停止词,那么停止词会被忽略。

BTree索引和B+Tree索引

1.BTree索引

BTree是平衡搜索多叉树,设树的度为d(d > 1),高度为h,那么BTree要满足以下条件:

  • 每个叶子节点的高度一样,等于h;
  • 每个非叶子节点由n-1个key和n个point组成,其中d<=n<=2d,key和point相互间隔,结点两端一定是key;
  • 叶子结点指针都为null;
  • 非叶子结点的key都是[key, data]二元组,其中key表示作为索引的键,data为键值所在行的数据。

在BTree的结构下,就可以使用二分查找的查找方式,查找复杂度为h*log(n),一般来说树的高度是很小的,一般为3左右,因此BTree是一个非常高效的查找结构。

2.B+Tree索引

B+Tree是BTree的一个变种,设树的度为d,h为树的高度,B+Tree和BTree的不同主要在于:

  • B+Tree中的非叶子结点不存储数据;
  • B+Tree的叶子节点没有指针,所有键值都会出现在叶子节点上,且key存储的键值对应的数据的物理地址。

结构如下:
20180411153126604

一般来说B+Tree比BTree更适合实现外存的索引结构,因为存储引擎的设计专家巧妙的利用了外存(磁盘)的存储结构,即磁盘的一个扇区是整数倍的page(页),页是存储中的一个单位,通常默认为4K,因此索引结构的节点被设计为一个页的大小,然后利用外存的“预读取”原则,每次读取的时候,把整个节点的数据读取到内存中,然后在内存中查找,已知内存的读取速度是外存读取I/O速度的几百倍,那么提升查找速度的关键就在于尽可能少的磁盘I/O,那么可以知道,每个节点中的key个数越多,那么树的高度越小,需要I/O的次数越少,因此一般来说B+Tree比BTree更快,因为B+Tree的非叶节点中不存储data,就可以存储更多的key。

四、索引的使用策略

什么时候要使用索引?

  • 主键自动建立唯一索引;
  • 经常作为查询条件在WHERE或者ORDER BY语句中出现的列要建立索引;
  • 作为排序的列要建立索引;
  • 查询中与其他表关联的字段,外键关系建立索引;
  • 高并发条件下倾向组合索引。

什么时候不要使用索引?

  • 经常增删改的列不要建立索引;
  • 有大量重复的列不建立索引;
  • 表记录太少不要建立索引。

注:

在组合索引中不能有列的值为NULL,如果有,那么这一列对组合索引就是无效的;
在一个SELECT语句中,索引只能使用一次,如果在WHERE中使用了,那么在ORDER BY中就不要用了;
LIKE操作中,'%aaa%'不会使用索引,也就是索引会失效,但是‘aaa%’可以使用索引;
在索引的列上使用表达式或者函数会使索引失效,例如:select * from users where YEAR(adddate)<2007,将在每个行上进行运算,这将导致索引失效而进行全表扫描,因此我们可以改成:select * from users where adddate<’2007-01-01′。
在查询条件中使用正则表达式时,只有在搜索模板的第一个字符不是通配符的情况下才能使用索引。
在查询条件中使用<>会导致索引失效。
在查询条件中使用IS NULL会导致索引失效。
在查询条件中使用OR连接多个条件会导致索引失效,这时应该改为两次查询,然后用UNION ALL连接起来。
尽量不要包括多列排序,如果一定要,最好为这队列构建组合索引;
只有当数据库里已经有了足够多的测试数据时,它的性能测试结果才有实际参考价值。如果在测试数据库里只有几百条数据记录,它们往往在执行完第一条查询命令之后就被全部加载到内存里,这将使后续的查询命令都执行得非常快--不管有没有使用索引。只有当数据库里的记录超过了1000条、数据总量也超过了MySQL服务器上的内存总量时,数据库的性能测试结果才有意义。

五、索引的优化

1. 最左前缀

索引的最左前缀和和B+Tree中的“最左前缀原理”有关,举例来说就是如果设置了组合索引<col1,col2,col3>那么以下3中情况可以使用索引:col1,<col1,col2>,<col1,col2,col3>,其它的列,比如<col2,col3>,<col1,col3>,col2,col3等等都是不能使用索引的。

根据最左前缀原则,我们一般把排序分组频率最高的列放在最左边,以此类推。

2. 带索引的模糊查询优化

在上面已经提到,使用LIKE进行模糊查询的时候,'%aaa%'不会使用索引,也就是索引会失效。如果是这种情况,只能使用全文索引来进行优化。

为检索的条件构建全文索引,然后使用

SELECT * FROM tablename MATCH(index_colum) ANGAINST(‘word’);

3. 不要在列上使用函数和进行计算

不要在列上使用函数,这将导致索引失效而进行全表扫描。

4. 负向条件查询不能使用索引

负向条件有:!=<>not innot existsnot like等等。

select * from article where status != 1 and status != 2;

可以使用 in 进行优化:

select * from article where status in (0, 3);

5. 范围列可以用到索引

范围条件有:<<=>>=between等。

范围列可以用到索引,但是范围列后面的列无法用到索引,索引最多用于一个范围列,如果查询条件中有两个范围列则无法全用到索引。

6. 避免强制类型转换

当查询条件两侧类型不匹配的时候会发生强制转换,强制转换可能导致索引失效而进行全表扫描。

如果 phone 字段是 varchar 类型,则下面的 SQL 不能命中索引:

select * from user where phone = 12345678901;

可以优化为:

select * from user where phone = '12345678901';

六、 索引的优缺点

优势:可以快速检索,减少I/O次数,加快检索速度;根据索引分组和排序,可以加快分组和排序;
劣势:索引本身也是表,因此会占用存储空间,一般来说,索引表占用的空间的数据表的1.5倍;索引表的维护和创建需要时间成本,这个成本随着数据量增大而增大;构建索引会降低数据表的修改操作(删除,添加,修改)的效率,因为在修改数据表的同时还需要修改索引表。

2019/02/21--一张表里面有ID自增主键,当insert了17条记录之后,删除了第15,16,17条记录,再把mysql重启,再insert一条记录,这条记录的ID是18还是15 ?

如果你的表类型是InnoDB,那么,新纪录的ID为15;如果你的表类型是MyISAM,那么,新纪录的ID为18。

这是因为:InnoDB类型的数据表将表最后的ID值保存在内存里面。所以,当我们重新启动服务器后,内存里面的数据清空,那么自增的ID将重新按照现有表的纪录计算;相反,如果是MyISAM类型的数据表,将最大纪录ID保持在文件里,这样,虽然,重启了服务器,下次插入新纪录的时候,自增ID通过读取文件而计算得到。

2018/04/26--[译文]机器学习简史

原文地址:https://www.forbes.com/sites/bernardmarr/2016/02/19/a-short-history-of-machine-learning-every-manager-should-read/#20efa15d15e7

机器学习之简史

机器人是否会梦到电子羊,这是一个好问题。而科学事实也已经发展到开始与科幻小说重合的地步。
虽然我们还没有自主的机器人为自身的生存危机挣扎——到目前为止——却越来越接近人们称之为“人工智能”的东西。
《银翼杀手》(英语:Do Androids Dream of Electric Sheep?),直译是《仿生人会梦见电子羊吗?》,是美国科幻小说作家菲利普·K·迪克的最重要作品之一,第一次出版是在1968年,并改编成1982年电影《银翼杀手》,而该书的很多元素和主题都用于2017年续集电影《银翼杀手2049》。

机器学习是人工智能的一个子领域,其中计算机算法用于从数据和信息中自主学习。在机器学习中,计算机不一定要明确编程,但要可以自行改变和改进它们的算法。

如今,机器学习算法可以让电脑能够与人类交流,能自动驾驶汽车,能编写并发布运动赛事报告,而且能够找到恐怖主义的嫌疑人。
我坚信机器学习将会严重影响大多数行业及其工作岗位,这也是为什么每个(产品/技术)经理至少应该掌握什么是机器学习以及它是如何演变的。

在这篇文章中,我准备了一个快速浏览机器学习起源及其最近的里程碑的时间之旅。

1950年——艾伦·图灵(Alan Turing)创造了“图灵测试”来确定计算机是否具有真正的智慧。要通过测试,计算机必须能够欺骗一个人类,让他相信它也是一个人类。

1952年——亚瑟·塞缪尔(Arthur Samuel)编写了第一个电脑学习程序。该程序是关于跳棋游戏的,旨在研究哪些走法构成了获胜策略然后将其纳入程序当中,计算机玩得次数越多则提高越多。

1957年——弗兰克·罗森布特(Frank Rosenblatt)设计了计算机的第一个神经网络(感知器),它可以模拟人脑的思维过程。

1967年——“邻近(最近邻)算法”(nearest neighbor)诞生,允许计算机开始使用非常基本的模式识别。这可以为出差的推销员制定路线,从一个随机城市开始,而能确保他们在短距离旅途期间到达所有城市。

1979年——斯坦福大学的学生发明了“斯坦福购物车”(Stanford Cart),可以在房间里自行导航识别障碍物。

1981年——杰拉德·德琼(Gerald Dejong)介绍了基于解释学习(EBL)的概念,其中计算机分析训练数据,并通过丢弃不重要的数据创建器可遵循的通用规则。

1985年——特里·塞杰瑙斯基(Terry Sejnowski)发明了NetTalk,它可以学习像婴儿一样发音。

20世纪90年代——(人们)致力于将机器学习从知识驱动转变为数据驱动方法。科学家们开始创建计算机程序来分析大料数据,并从结果中得出结论或“学习”。

1997年——IBM的深蓝计算机(Deep Blue)打败了国际象棋的世界冠军。

2006年——杰弗里·辛顿(Geoffrey Hinton)用属于“深度学习”来结束让计算机“看到”并区分图像、视频中的对象以及文本的新算法。

2010年——微软的Kinect可以30次每秒的速度追踪20个人的人物特征,让人们可以通过动作和手势与电脑进行互动。

2011年——IBM的沃森(能够使用自然语言来回答问题的人工智能系统)在《危险边缘》节目上打败了它的人类竞争者。《危险边缘》(英语:Jeopardy!)是由梅夫·格里芬在1964年创建的美国的电视智力竞赛节目。就像同一类的其它节目,节目涵盖了历史、语言、文学、艺术、科技、流行文化、体育、地理、文字游戏等多方面内容。然而,与这些节目不同的是,《危险边缘》采取一种独特的问答形式:参赛者须根据以答案形式提供的各种线索,以问题的形式作出正确的回答。

2011年——谷歌大脑被开发出来,它的深度神经网络可以像一只猫一样去发现和归类事物。

2012年——谷歌X实验室开发了一个机器学习算法,能够自动浏览YouTube来识别那些含有猫的视频。

2014年——Facebook开发了一个软件算法——DeepFace,该算法能够识别或验证照片上的个体,而且与人类能够达到的水平相同。

2015年——亚马逊推出了自己的机器学习平台。

2015年——微软创建了分布式的机器学习工具包,可以在多台计算机上高效地分配机器学习的问题。

2015年——超过3000名AI和机器人的研究人员,在史蒂芬·霍金、艾伦·马斯克和史蒂夫·沃兹尼亚克(等等)支持下,签署了一封公开信,警示人们在没有人为干预的情况下选择和参与目标,自动化武器的危险。

2016年——谷歌的人工智能算法打败了**棋盘游戏围棋的专业选手,围棋被公认为是世界上最复杂的棋盘游戏,比国际象棋要难上好多倍。AlphaGo由Google DeepMind开发,该算法在五场围棋比赛中均获得胜利。

那么我们是否越来越接近人工智能了呢?一些科学家认为这实际上是一个错误的问题。

他们认为一台计算机永远也无法像一个人类那样去“思考”,而将计算机的计算分析与算法与人类思维的诡计进行比较就像比较苹果和橘子,根本无法相比。

无论如何,计算机观察、理解,并且与世界互动的能力正在以惊人的速度增长。随着我们生产的数据量继续呈指数增长,我们的计算机处理、分析以及从数据增长和扩展的学习能力也会随之增强。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.