Difference between first() and take(1)

The difference is that take(1) will relay 0..1 items from upstream whereas first will relay the very first element or emits an error (NoSuchElementException) if the upstream is empty. Neither of them is blocking.

It is true first == take(1).single() where take(1) limits the number of upstream items to 1 and single() makes sure upstream isn't empty.

This example prints "Done" only

Observable.empty().take(1)
.subscribe(System.out::println, 
    Throwable::printStackTrace, 
    () -> System.out.println("Done"));

This example prints "1" followed by "Done":

Observable.just(1).take(1)
.subscribe(System.out::println, 
    Throwable::printStackTrace, 
    () -> System.out.println("Done"));

This example also prints "1" followed by "Done":

Observable.just(1, 2, 3).take(1)
.subscribe(System.out::println, 
    Throwable::printStackTrace, 
    () -> System.out.println("Done"));

This example fails with NoSuchElementException

Observable.empty().first()
.subscribe(System.out::println, 
    Throwable::printStackTrace, 
    () -> System.out.println("Done"));

This example, again, prints "1" followed by "Done":

Observable.just(1).first()
.subscribe(System.out::println, 
    Throwable::printStackTrace, 
    () -> System.out.println("Done"));

This example, again, prints "1" followed by "Done":

Observable.just(1, 2, 3).first()
.subscribe(System.out::println, 
    Throwable::printStackTrace, 
    () -> System.out.println("Done"));

This example prints a stacktrace of NoSuchElementException because the source contained too few elements:

Observable.empty().single()
.subscribe(System.out::println, 
    Throwable::printStackTrace, 
    () -> System.out.println("Done"));

This example prints a stacktrace of IllegalArgumentException because the source contained too many elements:

Observable.just(1, 2, 3).single()
.subscribe(System.out::println, 
    Throwable::printStackTrace, 
    () -> System.out.println("Done"));

The difference in the implementation is driven by the difference in the semantics. Without any knowledge of the language and its inner workings, consider what these two methods imply.

first() implies that it will return exactly one element. The first element in the collection.

take(x) implies that it will return a collection of elements. The first x elements in the collection.

Different semantics demand different names. And different return values demand different technical implementations.

It's also possible (again, without looking under the hood to get a technical answer), that the two implementations may handle error conditions very differently. I would personally expect first() to throw an exception for an empty collection, since there is no first element. However, I may reasonably expect take(x) to return a collection smaller than the size of x, without error, if the initial collection has fewer than x elements. Which may lead to returning, without error, an empty collection whenever given an empty collection.

Additionally, as a technical concern, something like take(x) is more likely to not actually iterate the underlying collection immediately but rather defer that until something iterates its result. first(), however, needs to iterate and materialize the first element of the underlying collection.