← Назад к вопросам

Сколько раз вызовется HTTP GET внутри Stream?

1.0 Junior🔥 171 комментариев
#Stream API и функциональное программирование

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Ленивое вычисление Streams: Сколько раз выполнится HTTP GET?

Это коварный вопрос, который показывает понимание lazy evaluation в Java Streams. Ответ: это зависит от кода, но обычно люди ошибаются.

Ошибочное понимание

Многие думают, что Stream вычисляет каждый элемент полностью, но на самом деле Streams ленивы.

Пример 1: Базовое понимание

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

Stream<Integer> stream = numbers.stream()
    .peek(n -> System.out.println("Processing: " + n))
    .filter(n -> n > 2)
    .peek(n -> System.out.println("After filter: " + n));

// На этом моменте: ВСЕ ОСТАЁТСЯ НЕПОДВИЖНЫМ
// Ничего не распечатает!

// Только когда вызовем terminal operation:
stream.forEach(n -> System.out.println("Final: " + n));

/* Вывод:
Processing: 1
Processing: 2
Processing: 3
After filter: 3
Final: 3
Processing: 4
After filter: 4
Final: 4
Processing: 5
After filter: 5
Final: 5
*/

Ключевой момент: Terminal operation (forEach, collect, count и т.д.) запускает вычисления.

Пример 2: HTTP GET в Stream

Это типичный вопрос на интервью:

public class UserService {
    private RestTemplate restTemplate;
    
    // ❌ НЕПРАВИЛЬНО: HTTP GET будет вызван многократно!
    public List<User> findActiveUsers(List<Integer> userIds) {
        return userIds.stream()
            .map(id -> fetchUserFromApi(id)) // HTTP GET здесь!
            .filter(user -> user.isActive())
            .collect(Collectors.toList());
    }
    
    private User fetchUserFromApi(Integer id) {
        System.out.println("Fetching user " + id);
        // HTTP GET запрос к API
        return restTemplate.getForObject(
            "http://api.example.com/users/" + id,
            User.class
        );
    }
}

// Использование
UserService service = new UserService();
List<Integer> userIds = Arrays.asList(1, 2, 3, 4, 5);
List<User> activeUsers = service.findActiveUsers(userIds);

// Сколько HTTP GET вызовов?
// Ответ: 5 вызовов (один для каждого ID)
// Потому что:
// 1. map() выполняет fetchUserFromApi для КАЖДОГО элемента
// 2. filter() просто проверяет условие
// 3. collect() это terminal operation, который запускает весь pipeline

Выход:

Fetching user 1
Fetching user 2
Fetching user 3
Fetching user 4
Fetching user 5

5 HTTP GET запросов! Это происходит потому, что map() — это intermediate operation, которая выполняется для каждого элемента при вызове terminal operation.

Пример 3: Пошаговое выполнение (short-circuit operations)

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// Использование limit() — это short-circuit operation
int result = numbers.stream()
    .peek(n -> System.out.println("Processing: " + n))
    .filter(n -> n % 2 == 0)
    .limit(2) // Остановить после 2 элементов
    .collect(Collectors.toList());

/* Вывод:
Processing: 1
Processing: 2
Processing: 3
Processing: 4
Processing: 5
Processing: 6
Processing: 7 — STOP!

Результат: [2, 4]
*/

// Вопрос: Сколько раз выполнилось peek()?
// Ответ: 7 раз (не 10!)
// Потому что limit(2) остановил processing когда нашёл 2 элемента

С HTTP GET это критично:

public List<User> findFirstActiveUsers(List<Integer> userIds) {
    return userIds.stream()
        .map(this::fetchUserFromApi) // HTTP GET
        .filter(user -> user.isActive())
        .limit(3) // Возвращаем только первых 3
        .collect(Collectors.toList());
}

// Сколько HTTP GET вызовов?
// Ответ: МОЖЕТ БЫТЬ МЕНЬШЕ чем 5!
// Зависит от когда мы встречаем 3 активных пользователей
// Worst case: все 5 если только последние 3 активны
// Best case: 3 если первые 3 активны

Пример 4: Кэширование результатов

// ✅ ПРАВИЛЬНО: Кэшируем результаты HTTP GET
private Map<Integer, User> userCache = new HashMap<>();

public List<User> findActiveUsers(List<Integer> userIds) {
    return userIds.stream()
        .map(id -> userCache.computeIfAbsent(id, this::fetchUserFromApi))
        .filter(user -> user.isActive())
        .collect(Collectors.toList());
}

// Вызов 1: 5 HTTP GET (все новые IDs)
List<User> result1 = findActiveUsers(Arrays.asList(1, 2, 3, 4, 5));

// Вызов 2: 0 HTTP GET (все из кэша)
List<User> result2 = findActiveUsers(Arrays.asList(1, 2, 3, 4, 5));

// Вызов 3: 2 HTTP GET (1, 2, 3, 4, 5 в кэше, 6 и 7 новые)
List<User> result3 = findActiveUsers(Arrays.asList(1, 2, 3, 4, 5, 6, 7));

Пример 5: Parallel Streams

// ⚠️ Параллельные Streams усложняют ситуацию
List<Integer> userIds = Arrays.asList(1, 2, 3, 4, 5);

List<User> users = userIds.parallelStream()
    .map(this::fetchUserFromApi) // HTTP GET может быть параллельным!
    .filter(user -> user.isActive())
    .collect(Collectors.toList());

// Сколько HTTP GET?
// Ответ: ВСЕГДА 5, но они выполняются параллельно
// Это НАМНОГО быстрее если API поддерживает параллельные запросы
// Но может перегрузить сервер API!

Пример 6: "Коварный" вопрос с multiple streams

List<Integer> userIds = Arrays.asList(1, 2, 3, 4, 5);

Stream<User> stream = userIds.stream()
    .map(this::fetchUserFromApi);

// Вопрос: Сколько HTTP GET на этом моменте?
// Ответ: 0! Потому что нет terminal operation

// Только когда:
List<User> users1 = stream.filter(u -> u.isActive()).collect(Collectors.toList());
// Теперь 5 HTTP GET

// Вопрос 2: Если повторно вызвать
List<User> users2 = stream.filter(u -> u.isAge() > 18).collect(Collectors.toList());
// Ответ: Exception! Поток уже использован
// java.lang.IllegalStateException: stream has already been operated upon or closed

Пример 7: Правильное решение для HTTP запросов

// ✅ Используем CompletableFuture для асинхронных HTTP запросов
public List<User> findActiveUsersAsync(List<Integer> userIds) {
    return userIds.stream()
        .map(id -> CompletableFuture.supplyAsync(() -> fetchUserFromApi(id)))
        .map(CompletableFuture::join) // Ждём все результаты
        .filter(user -> user.isActive())
        .collect(Collectors.toList());
}

// Сколько HTTP GET?
// Ответ: 5, но они выполняются асинхронно
// Общее время = максимум из всех запросов (не сумма)

// Ещё лучше: используем asyncHttpClient
public List<User> findActiveUsersOptimal(List<Integer> userIds) {
    return userIds.stream()
        .parallel() // Параллельно
        .map(this::fetchUserFromApiAsync) // Асинхронный HTTP
        .map(CompletableFuture::join)
        .filter(user -> user.isActive())
        .collect(Collectors.toList());
}

Таблица: Сколько раз выполнится операция

КодHTTP GET вызововПримечание
stream().map(::fetch).collect()NДля каждого элемента
stream().map(::fetch).filter().collect()Nfilter не влияет
stream().map(::fetch).limit(2).collect()2-NЗависит от условий
stream().map(::fetch).findFirst()~1Останавливается сразу
stream().map(::fetch).anyMatch()~NОстанавливается на true
parallelStream().map(::fetch)N parallelПо количеству потоков
stream().map(cache.computeIfAbsent(::fetch))только новыеБлагодаря кэшу

Key Takeaways

  1. Streams ленивы — intermediate operations не выполняются до terminal operation
  2. map() вызывает функцию для КАЖДОГО элемента — это не фильтрация
  3. filter() НЕ вызывает функцию — только проверяет условие
  4. Short-circuit operations (limit, findFirst, anyMatch) могут остановить processing досрочно
  5. С HTTP запросами это критично — неправильное использование Streams может привести к 100 ненужным запросам вместо 10
  6. Кэширование спасает — computeIfAbsent, memoization
  7. Parallel Streams быстрее, но опаснее — может перегрузить ресурсы

Ответ на типичный вопрос интервью

Вопрос: "Сколько раз вызовется GET запрос в этом коде?"

List<Integer> ids = Arrays.asList(1, 2, 3, 4, 5);
ids.stream()
   .map(id -> httpGet("user/" + id))
   .filter(user -> user.isActive())
   .collect(Collectors.toList());

Правильный ответ: "5 раз. Потому что map() выполняет httpGet для каждого ID, независимо от filter(). Stream ленив, но map() применяется к каждому элементу во время terminal operation (collect())."