作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO
联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬
烦人的空指针
请大家观察下面这段代码:
public class NullPointerTest {
/**
* 需求:根据用户名查找该用户所在的部门名称
*
* @param args
*/
public static void main(String[] args) {
String departmentNameOfUser = getDepartmentNameOfUser("test");
System.out.println(departmentNameOfUser);
}
/**
* 假设这是A-Service的服务
* 这一步很烦!!!
*
* @param username
* @return
*/
public static String getDepartmentNameOfUser(String username) {
ResultTO<User> resultTO = getUserByName(username);
if (resultTO != null) {
User user = resultTO.getData();
if (user != null) {
Department department = user.getDepartment();
if (department != null) {
return department.getName();
}
}
}
return "未知部门";
}
/**
* 假设这是B-Service的服务(不用关注具体逻辑,就是随机模拟返回值,可能为null)
*
* @param username
* @return
*/
public static ResultTO<User> getUserByName(String username) {
if (username == null || "".equals(username)) {
return null;
}
Department department;
User user;
if (ThreadLocalRandom.current().nextBoolean()) {
department = new Department("总裁办", 10086);
} else {
department = null;
}
if (ThreadLocalRandom.current().nextBoolean()) {
user = new User("周董", 18, department);
user.setDepartment(department);
} else {
user = null;
}
return ResultTO.buildSuccess(user);
}
@Data
@AllArgsConstructor
@NoArgsConstructor
static class User {
private String name;
private Integer age;
private Department department;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
static class Department {
private String name;
private Integer code;
}
@Getter
@Setter
static class ResultTO<T> implements Serializable {
private Boolean success;
private String message;
private T data;
public static <T> ResultTO<T> buildSuccess(T data) {
ResultTO<T> result = new ResultTO<>();
result.setSuccess(true);
result.setMessage("success");
result.setData(data);
return result;
}
public static <T> ResultTO<T> buildFailed(String message) {
ResultTO<T> result = new ResultTO<>();
result.setSuccess(false);
result.setMessage(message);
result.setData(null);
return result;
}
}
}
你会发现,如果一个POJO的层级过深而且恰好作为返回值返回时,调用者将苦不堪言,为了避免空指针不得不写一大堆的if判断,也就是被迫做空指针探测。
一种较为通用的做法是采用“卫函数”:
/**
* 假设这是A-Service的服务
* 这一步很烦!!!
*
* @param mx
* @return
*/
public static String getDepartmentNameOfUser(String username) {
ResultTO<User> resultTO = getUserByName(username);
if (resultTO == null) {
return "ResultTO为空";
}
User user = resultTO.getData();
if (user == null) {
return "User为空";
}
Department department = user.getDepartment();
if (department == null) {
return "Department为空";
}
return department.getName();
}
虽然避免了过深的if嵌套,逻辑稍微清晰一点,但还是很啰嗦。
封装NullWrapper
我们来尝试封装一个工具类,希望能简化NullPointerException的探测工作。
核心思想
设计一个Wrapper,内部有一个T value字段,用来接收返回值,这样就能把null包裹在内部,稍微安全了一些,因为Wrapper肯定不为null:new Wrapper(value).getXxx()。
但这还不够!因为如果这个value真的是null,而外界getValue()后再次调用的话,仍然会发生NullPointerException。
怎么处理?
其实答案已经出现过了:再次把返回值包装成Wrapper即可。
比如:
public static void main(String[] agrs) {
Wrapper resultWrapper = new Wrapper(getDepartmentnameByName(username));
Wrapper userWrapper = new Wrapper(resultWrapper.get()); // resultTO可能为null,所以再次包装
Wrapper departmentWrapper = new Wrapper(userWrapper.get());
Wrapper departmentNameWrapper = new Wrapper(departmentWrapper.get());
// ...
}
但上面的代码其实“很傻”:
从resultWrapper取出内部的值以后,又塞进新的wrapper...其实是没有意义的,最后那个departmentNameWrapper内部塞的其实还是username
我们应该对value进行判断,当value不为null时往下取一层,这样才能最终一层层剥开value得到最终的值:
public <U> NullWrapper<U> map(Function<? super T, ? extends U> mapper) {
Objects.requireNonNull(mapper);
if (value是否为null)
// 如果为null,直接用Wrapper包装null,避免直接暴露导致空指针
return new Wrapper(null);
else {
// 如果不为null,那么调用传入的映射规则,【剥掉一层嵌套】,把下一级取出来重新包装为Wrapper
return new Wrapper(mapper.apply(value));
}
}
由于不论value是否为null,最终返回的都是Wrapper,而Wrapper有map(),所以可以形成链式调用:
也就是说,每一次map()其实都有可能剥掉一层嵌套,而且过程中不会发生空指针异常!
// 初始化包裹,得到Wrapper
Wrapper wrapper = new Wrapper(firstLevelValue);
// 调用Wrapper#map()尝试向下剥开每一级的嵌套,由于map()返回的也是Wrapper对象,可以链式调用
wrapper
.map(firstLevelValue -> firstLevelValue.getSecondLevelValue)
.map(secondLevelValue -> secondLevelValue.getThirdLevelValue)
.map(...)
为什么是有可能?
我们来考虑两种极端的情况:
- 如果每一级value都为null,其实每次map()都是对null进行包装传递、取出、再包装,并没有剥开任何嵌套(null值不存在嵌套)
- 如果每一级value都不为null,每次map()都剥开一层,这是最理想的效果,离最终需要的value越来越近
第一种情况,其实也没什么,无非就是多new几个Wrapper对象(似乎有点浪费?后面再优化)。第二种情况,符合我们的预期,但也不能每次剥开又给套上Wrapper吧,丑媳妇最终还是要见公婆。所以,除了map()方法,Wrapper还需要额外提供一个最终获取真实value的方法,比如Wrapper#orElse(T other),它允许调用者得到未包装的value,但是!有个条件:调用orElse()时必须传一个备用的值,如果value真的为null,则返回备用值代替null。如果你实在没有备用值,也可以使用null作为备用值,但使用时务必谨慎。
上面讲述的就是Wrapper工具类的核心思想,接着让我们一起来封装一下!
为了更容易理解,我们先封装一个Mini版:
public final class MiniNullWrapper<T> {
/**
* 实际值
*/
private final T value;
/**
* 无参构造,默认包装null
*/
private MiniNullWrapper() {
this.value = null;
}
/**
* 有参构造器,用来包装外部传入的value
*
* @param value
*/
private MiniNullWrapper(T value) {
this.value = value;
}
/**
* 静态方法,返回一个包装了null的Wrapper
*
* @param <T>
* @return
*/
public static <T> MiniNullWrapper<T> empty() {
// 调用无参构造,返回包装了null的Wrapper
System.out.println("由于value为null,直接返回包装了null的Wrapper,让流程继续往下");
return new MiniNullWrapper<>();
}
/**
* 静态方法,返回一个包装了value的Wrapper(value可能为null)
*
* @param value
* @param <T>
* @return
*/
public static <T> MiniNullWrapper<T> ofNullable(T value) {
// 调用有参构造,返回包装了value的Wrapper
return new MiniNullWrapper<>(value);
}
/**
* 核心方法:
* 1.如果value为null,直接返回空的Wrapper
* 2.如果value不为null,则使用mapper对value进行处理,往下剥一层(这是关键,一有机会就要往下剥一层,否则就是原地踏步)
*
* @param mapper
* @param <U>
* @return
*/
public <U> MiniNullWrapper<U> map(Function<T, U> mapper) {
Objects.requireNonNull(mapper);
if (value == null)
// 按上面说的,如果value为null,我都不处理了。但为了调用者拿到返回值后不会发生空指针,需要用Wrapper包装一下
return MiniNullWrapper.empty();
else {
/*
* value不为null,那么就要想尽办法将它剥去一层皮。
* 由于此时value不为null,即使mapper的apply方法要做的操作是 value.getXxx()/value.setXxx(),都不会空指针
* mapper.apply(value)处理后的结果继续用Wrapper包装,此时【新的wrapper里的value】是处理后的数据(下一层)
* */
return MiniNullWrapper.ofNullable(mapper.apply(value));
}
}
/**
* 终端操作,决定勇敢一次。当你做好面对外面的世界时,就要卸下伪装:直接把value丢出去。
* 但为了不祸害别人,给个备选值:other。当你确实是null时,返回other。
*
* @param other
* @return
*/
public T orElse(T other) {
return value != null ? value : other;
}
// -------- 测试方法 ---------
public static void main(String[] args) {
// 全部不为null
Son sonNotNull = new Son("大头儿子");
Father fatherNotNull = new Father();
fatherNotNull.setSon(sonNotNull);
GrandPa grandPaNotNull = new GrandPa();
grandPaNotNull.setFather(fatherNotNull);
// 处理grandPa,观察map()中的处理方法有没有被调用
String sonName1 = MiniNullWrapper.ofNullable(grandPaNotNull)
.map(grandPa -> grandPa.getFather())
.map(father -> father.getSon())
.map(son -> son.getName())
.orElse("没得到儿子的名字");
// 全部为null
// GrandPa grandPaNull = new GrandPa();
// grandPaNull.setFather(null);
// // 处理grandPa,观察map(),你会发现,从grandPa取出father后,由于发现是null,所以father->father.getSon()不会执行,避免了空指针
// String sonName2 = MiniNullWrapper.ofNullable(grandPaNull)
// .map(grandPa -> grandPa.getFather())
// .map(father -> father.getSon())
// .map(son -> son.getName())
// .orElse("没得到儿子的名字");
}
// ---- 没啥实质内容,就是几个简单的类,我在getter方法中打印了一些信息 ----
static class GrandPa {
private Father father;
public Father getFather() {
System.out.println("GrandPa#getFather被调用了");
return father;
}
public void setFather(Father father) {
this.father = father;
}
}
static class Father {
private Son son;
public Son getSon() {
System.out.println("Father#getSon被调用了");
return son;
}
public void setSon(Son son) {
this.son = son;
}
}
@AllArgsConstructor
static class Son {
private String name;
public String getName() {
System.out.println("Son#getName被调用了");
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
大家先把上面的代码消化了,然后再重新思考下面的问题
MiniNullWrapper.ofNullable(result)
.map(step1)
.map(step2) // 是否曾担心,在这一步突然出现null,然后空指针异常呢?
.map(step3)
.orElse(step4);
学习MiniNullWrapper后,是否已经有答案了呢?
所以,当你以后使用NullWrapper的map()时,大胆地往下“剥”,不要担心中间是否会出现null导致链路中断:如果value为null,压根不会执行你传入的mapper.apply(),而是创建空的Wrapper继续往下传递!
工具类封装
来看看完全版的NullWrapper吧~
/**
* 工具类,用于简化空指针的探测工作
* 核心思想:将可能为null的value包装成NullWrapper对象,那么调用nullWrapper.xxx()方法肯定就不会发生NullPointerException
* 核心方法:map()
* <p>
* map()方法实际操作的是nullWrapper对象内部的value,将value映射为指定的值(比如下一级的字段)
* 如果value为null,调用empty()方法,返回包装了null的NullWrapper对象
* 如果value不为null,执行mapper.apply(value)得到结果并调用ofNullable(T newValue)方法,返回包装了newValue的NullWrapper对象
*
* @param <T>
*/
public final class NullWrapper<T> {
/**
* 配合{@link NullWrapper#empty()}使用,返回一个包装了null的NullWrapper
* 主要是为了避免每次重复创建空的Wrapper!
*/
private static final NullWrapper<?> EMPTY = new NullWrapper<>();
/**
* 实际值
*/
private final T value;
/**
* 构造器,包装null
*/
private NullWrapper() {
this.value = null;
}
/**
* 构造器,包装指定的【非空值】
* 如果传入null会抛NullPointerException
*
* @param value
*/
private NullWrapper(T value) {
this.value = Objects.requireNonNull(value);
}
/**
* 静态方法,返回一个包装了null的NullWrapper
*
* @param <T>
* @return
*/
public static <T> NullWrapper<T> empty() {
@SuppressWarnings("unchecked")
NullWrapper<T> t = (NullWrapper<T>) EMPTY;
return t;
}
/**
* 静态方法,包装指定的【非空值】
* 如果传入null会抛异常NullPointerException
*
* @param value
* @param <T>
* @return
*/
public static <T> NullWrapper<T> of(T value) {
return new NullWrapper<>(value);
}
/**
* 静态方法,包装指定值,允许null。当传入null时,会调用empty()方法返回EMPTY对象
*
* @param value
* @param <T>
* @return
*/
public static <T> NullWrapper<T> ofNullable(T value) {
return value == null ? empty() : of(value);
}
/**
* 是否有值(非null)
*
* @return
*/
public boolean isPresent() {
return value != null;
}
/**
* 核心方法
* 调用者:NullWrapper对象,内部含有value,可能为null,也可能不为null
* 如果value为null,调用empty()方法,返回包装了null的NullWrapper对象
* 如果value不为null,执行mapper.apply(value)得到结果并调用ofNullable(T newValue)方法,返回包装了newValue的NullWrapper对象
* 简而言之,每次调用map(),都有可能剥掉一层外壳,也意味着躲过了一次潜在的NullPointerException
*
* @param mapper
* @param <U>
* @return
*/
public <U> NullWrapper<U> map(Function<T, U> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent())
return empty();
else {
return NullWrapper.ofNullable(mapper.apply(value));
}
}
/**
* 终端操作,当最终结果还是为null时,返回默认值other
*
* @param other
* @return
*/
public T orElse(T other) {
return value != null ? value : other;
}
// ---------- 对Object方法重写,不用关注 -----------
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof NullWrapper)) {
return false;
}
NullWrapper<?> other = (NullWrapper<?>) obj;
return Objects.equals(value, other.value);
}
@Override
public int hashCode() {
return Objects.hashCode(value);
}
@Override
public String toString() {
return value != null
? String.format("Optional[%s]", value)
: "Optional.empty";
}
}
不知道大家有没有发现一个小心思:
对于连续为null的情况,其实并不会每次都重新new Wrapper(),而是直接调用empty()返回EMPTY,也就是空Wrapper复用。
所以,即使嵌套的都是null也不会每次都new Wrapper()哟~
Demo重构
/**
* 假设这是A-Service的服务
*
* @param username
* @return
*/
public static String getDepartmentNameOfUser(String username) {
// 请求远程方法
ResultTO<User> resultTO = getUserByName(username);
// 包装为Wrapper
NullWrapper<ResultTO<User>> resultTONullWrapper = NullWrapper.ofNullable(resultTO);
// 链式调用
return resultTONullWrapper
.map(ResultTO::getData)
.map(User::getDepartment) // 只有User不为null时,方法引用才会被调用,所以不用担心空指针,大胆的传入Lambda/方法引用吧!(其他同理)
.map(Department::getName)
.orElse("未知部门");
}
简写成:
/**
* 假设这是A-Service的服务
*
* @param username
* @return
*/
public static String getDepartmentNameOfUser(String username) {
return NullWrapper.ofNullable(getUserByName(username))
.map(ResultTO::getData)
.map(User::getDepartment)
.map(Department::getName)
.orElse("未知部门");
}
是不是很简洁、很优雅呢~
Optional
其实上面的工具类就是模仿Java8 Optional写的,也已经演示了Optional最最核心的用法,所以这里不打算再对Optional进行长篇大论。
这里帮大家梳理一下Optional的API,标出几个重点且适合我们使用的方法:
被误解的Optional
Optional和Stream虽然都是Java8的新特性,但据我观察Optional的使用频率远低于Stream,究其原因是大家对它有误解。很多人以为Optional是用来“消除”空指针的,所以当他们发现即便使用了Optional还会抛异常时,感到非常地失望,甚至是愤怒。比如当value确实为null时,直接调用Optional#get()会抛出NoSuchElementException:
// Optional#get()底层源码,当value为null时抛出NoSuchElementException,虽然不是NPE,但也是异常
public T get() {
if (value == null) {
throw new NoSuchElementException("No value present");
}
return value;
}
这实在错怪Optional了!NPE是Java语言机制的一环,单靠一个Optional类如何能够消除呢?Optional的目的不是“消除空指针”,而是优雅地做空指针“探测”。就好比给了你一个排雷工具,但你就是不按正确方法使用它,最终被炸死了,这能怪谁呢?地雷是客观存在的,不可消除。你能做的就是好好利用排雷工具,避免地雷引爆。
再说回上面的Optional#get(),很多人觉得:妈的,好不容易Optional包装了null,结果又提供了一个可能抛异常的get方法,意义何在?实际上NPE之所以让人讨厌,不仅仅因为它是一个异常(我们日常开发遇到的异常还少吗),而是因为NPE往往会掩盖确切的错误信息。举个例子:
public void method1() {
User user = userService.getById(1L);
this.method2(user, 999);
}
public void method2(User user, Integer point) {
// 省略10+代码
updatePoint(user.getId(), point);
}
抛异常的是第8行的updatePoint()方法,而实际上“错误源头”是第2行的user,这会给我们排查问题造成干扰,特别是实际项目中往往调用链路更加复杂。如果使用Optional#get(),那么在get获取user的时候就会直接报错,排查问题会简单很多!
我个人基本不用Optional#get(),更习惯用orElse或orElseThrow()处理
令人困惑的isPresent()
public static String getDepartmentNameOfUser(String username) {
/**
* 把isPresent()当做判断空指针的方法,又回归以前的if嵌套,意义不大
* 个人觉得isPresent()不应该暴露出来,放在Optional内部使用更好
*/
Optional<ResultTO<User>> result2 = Optional.ofNullable(getUserByName("test"));
if (result2.isPresent()) {
User user = result2.get().getData();
if (user != null) {
// ...继续if判断空指针
}
}
}
当然,上面只是我个人觉得不合适的用法(JPA返回值就是Optional,很多人也是直接使用isPresent判空)。实际使用时,可以根据情况灵活应对。比如,当前得到Optional后已经是最后一步了,那么可以直接这样:
// 筛选出最大折扣的优惠券。max()返回的是Optional类型,但由于后面不再做get操作,可以直接用orElse(null)返回
Coupon maxDiscountCoupon = coupons.stream().max(Comparator.comparingDouble(Coupon::getDiscount)).orElse(null);
return maxDiscountCoupon;
如果真的需要判断是否存在并做进一步操作,对于单级判断可以用ifPresent()代替isPresent(),对于多级判断可以先用map()在用ifPresent()或orElse()等。
另外,由于Optional并没有实现Serializable接口,所以不推荐直接作为pojo字段使用。虽然可以作为内部方法的返回值,免去调用时手动包装,但这意味着强制调用者使用Optional,可能影响办公室和谐。应该把Optional当做一个简单的工具类,仿佛是你身边的同事封装的,不要对它抱有过分的期待,它的使命就一个:简化冗余的空指针探测。
推荐使用场景
第一个场景就是简化空指针探测,比如:
public static String getDepartmentNameOfUser(String username) {
ResultTO<User> resultTO = getUserByName(username);
if (resultTO != null) {
User user = resultTO.getData();
if (user != null) {
Department department = user.getDepartment();
if (department != null) {
return department.getName();
}
}
}
return "未知部门";
}
解决办法就是3个步骤:
- 包装value:Optional.ofNullable()
- 逐层安全地拆解value:map()
- 最终返回:orElse()/orElseGet()/orElseThrow
public static String getDepartmentNameOfUser(String username) {
return Optional.ofNullable(getUserByName(username))
.map(ResultTO::getData)
.map(User::getDepartment)
.map(Department::getName)
.orElse("未知部门");
}
其他的还可以是:
public boolean sendMessage(Long fromId, Long toId, String message) {
// 用户校验:如果用户不存在,直接抛异常
User user = Optional.ofNullable(userService.getUserById(fromId))
.orElseThrow(() -> new BizException(ErrorEnumCode.USER_NOT_EXIST));
// 组装数据并发送...
}
public List<String> listSubCities(String provinceCode) {
// 查到就返回,查不到就返回替代值(对于集合而言,尽量返回空集合)
return Optional.ofNullable(getCitiesByPid(provinceCode)).orElse(new ArrayList<String>());
}
另外,如果你需要对返回值进行判断,比如结果是否大于某个值等,可以使用Optional的filter方法。
public class OptionalFilterTest {
public static void main(String[] args) {
// 需求:调用getUser()得到person,并且person的age大于18才返回username,否则返回不存在
// 普通的写法(如果层级深一点会很难看)
Person user = getUser();
if (user != null && user.getAge() > 18) {
System.out.println(user.getName());
} else {
System.out.println("不存在");
}
// 你尝试用map(),但你发现直接返回username了,你甚至无法再次判断是否age>18
String username1 = Optional.ofNullable(getUser())
.map(Person::getName)
.orElse("不存在");
System.out.println("username1 = " + username1);
// 引入filter()
String username2 = Optional.ofNullable(getUser())
.filter(person -> person.getAge() > 18)
.map(Person::getName)
.orElse("不存在");
System.out.println("username2 = " + username2);
}
public static Person getUser() {
if (RandomUtils.nextBoolean()) {
return null;
} else {
Person person = new Person();
person.setName("鲍勃");
// commons.lang3
person.setAge(RandomUtils.nextInt(0, 50));
return person;
}
}
@Data
static class Person {
private String name;
private Integer age;
}
}
在很长一段时间里,我都不知道filter()方法存在的意义,因为Optional本身可以保证不发生空指针异常,那么我随便map()即可,反正最后orElse()兜底。但通过上面的案例,你应该也明白了:map()虽然好用,但也存在局限性。
最后,由于Optional不如Stream那么具有革命性,所以实际工作中没那么普及,甚至让不了解Optional的同事感到困惑。经过这篇文章的学习,相信你已经爱上它,可以尝试在自己团队中推广~
差点忘了,Stream API其实也使用了Optional:
private static void getMax() {
List<Department> list = new ArrayList<>();
list.add(new Department("总裁办", 1));
list.add(new Department("财务部", 2));
list.add(new Department("安保部", 3));
Optional<Department> max = list.stream().max(Comparator.comparingInt(Department::getCode));
Optional<Department> min = list.stream().min(Comparator.comparingInt(Department::getCode));
Optional<Department> first = list.stream().findFirst();
Optional<Department> any = list.parallelStream().findAny();
}
Optional并不是消除空指针判断,而是把判断逻辑塞进了诸如map()等方法中,再配合链式调用,让代码更简洁精炼。
Java8 Optional的不足
如果你从未用过Optional,那么这一小节要讲的东西你可能无法感同身受。这里举一个场景:
public List<UserTO> listUser(List<Long> userIds) {
return Optional.ofNullable(userDao.listUser(userIds))
.map(...) // 我想在这一步把所有UserDO转为UserTO
.orElse(Collections.emptyList());
}
但实际上Optional的map()是没有“遍历操作”的含义的。什么意思呢?对于:
userList.stream()
.map("把UserDO转为UserTO")
.collec(Collectors.toList())
这里的map其实是在一个循环里进行的,对于userList中的每个UserDO都会执行“UserDO转UserTO”的操作。但Optional的map()只能对userList整体进行转换,而不是对userList中的每一个UserDO(好好理解一下)。
在日常开发过程中,遇到这种情况我通常只能这样写:
public List<UserTO> listUser(List<Long> userIds) {
return Optional.ofNullable(userDao.listUser(userIds))
.map(userList -> {
// 内嵌一个stream
return userList.stream().map(userDO -> {
UserTO userTO = new UserTO();
// 拷贝UserDO属性到UserTO
return userTO;
}).collect(Collectors.toList());
})
.orElse(Collections.emptyList());
}
倒不是说看起来不够优雅啥的,只是如果把代码思路比作水流的话,每次用Optional处理List类型的数据并且需要做TO转换时,总觉得思想的水流到这里就遇到了一块大石头,总不免要荡起水花。
据说JDK9对Optional做了扩展,大家有兴趣可以了解下。
总结
我的体会是,Optional并不难,就是一个工具类,没几行代码,并且不存在丝毫的继承关系。难的是很多人不知道什么时候该用Optional、怎么用Optional。
如果大家去百度或知乎搜索“Optional”,会发现许多文章写得并不好,它们举的例子非常空洞,我甚至觉得作者其实根本没用过Optional,就是为了介绍每一个方法而强行弄出一个例子,最终我们虽然看完了整篇文章,却无法灵活应用Optional。
还是要靠大家自己实际开发时多体会体会。
作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO进群,大家一起学习,一起进步,一起对抗互联网寒冬