跳至主要內容

怎么匹配加分项:至少熟悉或了解一门后端语言

萌萌哒草头将军大约 13 分钟后端简历简历

一、说明

声明:本文为原创文章,未经许可禁止转载

虽然有点标题党,但本文旨在帮助读者增加薪酬谈判的底气和筹码。

众所周知,现在前端的招聘加分项要求里多半会有一条:至少熟悉或了解一门后端语言(java/python/golang)。本文主要以Java为例。

另外注意,简历上只需写自己最熟悉的一门后端语言就行,同时也从下面选择一个自己容易理解的语言。

二、怎么问你

作为前端,虽然经常和后端打交道,但是很难有机会系统学习某个后端语言,所幸,前端面试的时候一般会将JavaScript和Java两门语言的比较作为面试题目。比如:说说JavaScript和Java的异同点、JavaScript和Java相比有啥优点或缺点。

所以我们在面试前多做这类问题的准备。

下面就是你需要提前准备并且熟悉的知识点,

我将知识点分为了青铜黄金铂金,分别代表基础语法、语言特性、语言设计,面试的时候分别说出一两个点(十分推荐带星号标记的),这样粗中有细,有深有浅,面试官就会觉得你是真的了解而不是滥竽充数。如果被问道这个问题,你再假装思索一下(睁大眼睛,眼球向右上微微瞅一两秒)然后娓娓道来,效果更佳哦。

三、这么回答

青铜

1.语言类型

JavaScript是弱类型语言,边解释边执行,一个非const声明的变量可以保存多种类型的值。

// js
let num = 2022
let str = num.toString()
console.log(str)

而Java是强类型语言,先编译后执行,在编译前所有变量类型都是确定的,Java中如果一个变量接受另外类型的值,需要强制类型转换。另外,Java中使用final关键字定义不可变的常量

// java
// 声明前需要指定变量类型
Integer num = 2022;
String str = num.toString();
System.out.Println(str);
2.包装类*

JavaScript基本类型和包装类是一样的,基本类型可以调用类方法。

// js
let a = 2022
a.toString()

而Java基本类型无法调类方法,必须自动装箱成为包装类才能调用类型方法。

// java
int num = 2022
num.toString() // error

下面演示下Java的自动装箱和拆箱

// 没有自动装箱,声明一个包装类是这样的
Integer num1 = new Integer(2022);

// 有自动装箱,声明一个包装类是这样的
Integer num2 = 2022;

// 反过来就是自动拆箱
int num3 = num1;

// num1、num2是可以调用Integer类型方法的,比如toString,因为它们是包装类。
// num3是没法调用任何方法的,因为是基本类型。
image.png
image.png
3.number类型的差异*

JavaScript中数字类型只有number类型一种,而Java中数据类型字节数从小到大分为byte、short、int、long、double类型。所以JavaScript数字的转换是自动的,而Java中,小字节转换为大字节是自动转换,但是大字节转换为小字节,需要强制转换。

// java
byte a = 1;
// 由小到大
short b = a;

int c = 100;
// 由大到小需要手动指定
byte d = (byte)c;
4.==的区别*

JavaScript的==会进行隐式类型转换,然后按值比较。如果都是引用类型,只比较引用地址,如果是不同的类型,则会都会转换为同类型比较;

// js
0 == false // true

'1' == 1 // true

'hello' == 'he' + new String('llo') // true

而Java中==是严格按地址比较,地址相同时才相等,Java中的equals是严格按值比较的;同时Java中没有===

// java
0 == false // false

'1' === 1 // false

"hello" == "he" + new String("llo") // false
5.数组的异同

JavaScript的数组是任意长度的,并且可以存放各种类型。

// js
const arr = []
arr.push(...[1, '2', false, {}])

Java中的数组是固定长度的固定类型的,如果要像JavaScript使用数组,需要使用Java集合框架里的ArrayList或者LinkedList。

// java
int[] arr = new int[5];
arr[0] = 1;
arr[1] = 2;

List<Object> list = new ArrayList<Object>();
list.add(1);
list.add("2");

黄金

6.面向对象*

JavaScript虽然可以面向对象编程,但是它不符合面向对象编程的编程方式,class语法仅仅是prototype的语法糖,而Java是标准的面向对象的编程语言,天生具有面向对象特性:封装、继承、多态(多态的表现:重载、重写),JavaScript中没有封装和多态,所以没有重载,JavaScript的“重写”仅仅是prototype“短路”假象(因为子类有这个方法就不会沿着prototype属性向上查询)

// java
class Animal {
	String name;
	int age;
}
class Cat extends Animal {
	int fish;
	public Cat (String name, int age, int fish) {
		this.name = name;
		this.age = age;
		this.fish = fish;
	}
	public void call () { System.out.println("I am "+ this.name +", I have " + this.fish + "小鱼干"); }
}

class Dog extends Animal {
	int bone;
	public void Cat (String name, int age, int bone) {
		this.name = name;
		this.age = age;
		this.bone = bone;
	}
	public void call () { System.out.println("I am "+ this.name +", I have " + this.bone + "小鱼干"); }
}

public static void main (String []args) {
	Cat cat = new Cat("cat", 1, 2);
	cat.call();
	
	Dog dog = new Dog("dog", 2, 3);
	dog.call();
}
7. 私有属性和公有属性

JavaScript使用对象的defineProperty或者proxy方法可以限制对象属性私有还是公有。

// js
Object.defineProperty(window, 'pi', {
     writable:false,
     value: 3.14
})

Java中使用修饰符private或者public控制。

// java
// 公有
public float pi = 3.14

// 私有private float pi = 3.14

铂金

8. 垃圾回收(GC)*

不管什么语言,垃圾回收的整体策略都是一样的:先判断这块内存是否可回收,然后对可回收的内存使用回收器进行回收。

引用计数

怎么确定内存是否可回收的,业界的第一种做法是引用计数:如果一个对象被引用,就给这个对象的引用计数+1,如果不再使用这个对象,那就给这个对象的引用计数-1,每次触发GC流程时清除引用计数为零的对象。

标记-清除

引用计数基本是任何编程语言GC标配了,但是引用计数容易因为互相引用导致内存泄露,所以又出现了标记-清除:所有变量在进入内存前没有标记,如果对象被使用就将其标记,每次触发为GC流程时,清除没有被标记变量的内存。从上述可以发现,该策略分为两步:首先标记,然后清除

image.png
标记-清除也存在很大的缺陷:

  1. 每次运行GC时是扫描所有的变量,而有些变量是常驻变量。
  2. 清理掉的内存会成为内存碎片,导致内存成为不连续的片段。

所以很多编程语言在具体设计GC时,基于上述策略做了很多类似的优化。比如使用分代回收,避免每次扫描常驻对象,从而提升GC效率;将清理后的内存空间重写分配整理,避免形成内存碎片而浪费内存空间。

另外,所有的清除阶段,代码都是STW(Stop The World)状态。所以怎么缩短STW的时间也是各语言努力优化的目标。

image.png
image.png
v8引擎

不管是JavaScript还是Nodejs,都是运行在V8引擎(Chrome内核浏览器)上的,v8引擎为JavaScript的GC做了分代回收并行回收的优化,同时升级了标记-清除方案。

分代回收

image.png
首先将内存分为新生代区域和老生代区域,新生代里存放存活周期短的对象,老生代里存放存活周期长的常驻对象,这么做的好处就是,不用对一些常驻的对象频繁的做回收扫描,其次,又将新生代区域一分为二,一半作为使用区,一半作为空闲区。

新声明的变量会存入使用区,当使用区的剩余容量不足一存放新对象时,就会出发GC,大致的过程是将还在使用中(可达性分析确定的)对象复制到在空闲区然后清理,不使用的对象直接清理掉,然后将现在的空闲区标记为使用区,清理之后的使用区标记为空闲区。

并行回收

对于上述的新生代采用并行回收的方式。并行回收就是使用多个线程和主线程执行GC流程,并行回收的好处是可以缩短GC时间(不是成倍缩短,因为线程的通信也会消耗时间)。

image.png
image.png

标记清除和三色标记法

对于上述中的老生代采用标记清除的方法,它仍然是STW的,所以为了缩短STW时间,又将标记阶段切分为多个小段,每执行一小段就继续执行JavaScript代码,然后又执行小段的GC,反反复复......,v8将此称为增量标记
image.png
起初的标记清理,只是非黑即白的标记方式,如果在增量标记的场景下,当一小段增量标记完,下一小段增量标记开始时无法得知标记状态,所以V8采用了三色标记清除:未被标记时为白色、自身被标记但是成员未被标记时为灰色、自身和成员都被标记时是黑色。这样每段的增量标记都可以接着上段的标记继续工作了。

image.png
image.png
Java

Java使用引用计数和可达性分析作为GC策略,使用标记清除、标记整理、复制等GC方式

可达性分析

可达性分析是从GC Roots作为起始对象,依次寻找依赖的子对象,直至找不到依赖对象,如果此时对象没有和GC Root相连通,就会被判别为可回收对象(严格来说是准可回收,之后还有严格的验证措施)。

image.png
image.png

标记整理

将使用中的对象移到内存的另一端,将未使用的对象标记为可清除。

image.png
image.png

复制

将内存一分为二,一半作为使用区,一半作为空闲区,当使用区内存不足时,触发GC,将使用中的对象复制到空闲区,使用区的内存清空作为空闲区,前面的空闲区作为使用区,是不是很熟悉啊,没错V8新生代确实是借鉴了这里。

image.png
image.png

分代清理

Java的分代清理和JavaScript一样也是将内存分为新生代和老生代(永生代已经被移除了),不同的是,Java的新生代又被分为了三块,依次是:较大的Eden、较小的fron(s0)和to(s1)区,默认比例8 : 1 : 1 。对象优先在Eden区和from区,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC(新生代特有的GC算法),此时Eden区存活下来的变量将会被分配到to区,from区存活下来的变量年龄阈值,就会进入老年代,否则进入to区。此时清空Eden区和from区,然后将to区和from区调换身份。等待下一轮Minor GC

image.png
新生代使用的是Minor GC,老年代使用的是Full GC

10.异步编程
JavaScript

JavaScript当时仅仅用来和服务端交互,所以被设计成单线程语言(语言本身,浏览器是多进程多线程的),异步编程时只能采用回调函数或者Promise等方式,也没有并发。

Java

Java是多进程多线程语言,多线程就已经可以满足日常的并发需求了。不过多线程都会涉及线程状态和消息同步的问题。

Java的线程状态

一个线程被创建后成为初始(新建)状态,当调用start()之后进入就绪状态,表示可以被系统调用分配系统资源,当线程拿到系统分配的资源会调用run()方法,进入运行中状态,当线程失去系统分配的资源,比如执行了sleep(睡眠)suspend(挂起)就进入了阻塞状态。一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态

image.png
image.png

消息同步

线程同步消息的方式是:基于Java内存模型(JMM)的内存共享和使用wait()notify()消息传递。内存共享时,多个线程对同一个全局变量进行写操作时,是可能造成冲突的(线程安全问题),解决的方案就是增加安全机制:当一个线程对一个变量进行写操作时,其余想要对这个变量进行写操作的线程必须等待该线程写操作结束,Java中实现这个功能,有两种方法:synchronized(Volatile是轻量级的同步,只能修饰变量)、ThreadLock

synchronized既可以修饰方法成为同步方法也可以包裹需要同步的代码块成为同步代码块。

//java
// 同步代码块
public void setCount () {
   synchronized (lock) {
       this.cout ++;
   }
}
// 同步方法
public synchronized void setCount () {
   synchronized (lock) {
       this.cout ++;
   }
}

如果同步代码里又包了别的同步代码,就会形成死锁.

以Java为例创建线程,需要实现Runable接口,或者继承Thread类(本质也是实现了Runable接口)。

// java
class ThreadTest extends Thread {
   // 保存当前线程
   private Thread t;
   // 线程名
   private String threadName;
   // 锁
   private Object lock;
   // 操作对象
   int count = 0;
   
   ThreadTest ( String name) {
      threadName = name;
      System.out.println("创建了线程:" +  threadName );
   }
   
   public void run() {
      System.out.println("线程" +  threadName + "运行中");
      try {
         while(true) {
            System.out.println("线程: " + threadName);
            // 做点啥吧
            setCount()
         }
      }catch (InterruptedException e) {
         System.out.println("线程 " +  threadName + " interrupted.");
      }
      System.out.println("线程 " +  threadName + " 结束了");
   }
   
   public void start () {
      System.out.println("开始线程:" +  threadName );
      if (t == null) {
         t = new Thread (this, threadName);
         t.start();
      }
   }
   // 同步代码块
   public void setCount () {
       synchronized (lock) {
           this.cout ++;
       }
   }
   // 同步方法
   public synchronized void setCount () {
       synchronized (lock) {
           this.cout ++;
       }
   }
}

但是使用多进程的最大的缺点是进程之间消息通信、Cpu上下文切换消耗很大,所以使用过多的线程并发编程,效率反而降低了。

四、结束

本文到此就结束了,感兴趣的话可以关注我的微信公众号:萌萌哒草头将军

祝大家都能拿到满意的offer